From f89452acdd3d0e209a4416ac0d3b701af2a14e1b Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Fri, 25 Oct 2024 19:39:11 +0800 Subject: [PATCH] Group role (#2993) * feat: app/dataset support group (#2898) * pref: member-group (#2862) * feat: group list ordered by updateTime * fix: transfer ownership of group when deleting member * fix: i18n fix * feat: can not set member as admin/owner when user is not active * fix: GroupInfoModal hover input do not change color * fix(fe): searchinput do not scroll * feat: app collaborator with group, remove default permission * feat: dataset collaborator with group, remove default permission * chore(test): pref mock * chore: remove useless code * chore: adjust * fix: add self as collaborator when creating folder * fix(fe): folder manage menu do not show when user has write permission only * fix: dataset folder create * feat: Add code comment * Pref: app move (#2952) * perf: app schema * doc --------- Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> --- .../zh-cn/docs/development/upgrading/4812.md | 10 +- packages/global/core/app/collaborator.d.ts | 10 +- packages/global/core/app/type.d.ts | 11 +- .../global/core/dataset/collaborator.d.ts | 5 +- packages/global/core/dataset/type.d.ts | 12 +- .../support/permission/collaborator.d.ts | 6 +- .../global/support/permission/constant.ts | 1 - packages/global/support/permission/type.d.ts | 5 + packages/service/core/app/schema.ts | 9 +- packages/service/core/dataset/schema.ts | 10 +- .../service/support/permission/app/auth.ts | 13 +- .../service/support/permission/controller.ts | 85 ++++++- .../support/permission/dataset/auth.ts | 20 +- .../support/permission/inheritPermission.ts | 37 +-- .../permission/memberGroup/controllers.ts | 2 +- packages/service/support/permission/type.d.ts | 1 + .../service/support/permission/user/auth.ts | 2 +- packages/web/i18n/zh/app.json | 1 + packages/web/i18n/zh/common.json | 7 +- packages/web/i18n/zh/dataset.json | 5 +- pnpm-lock.yaml | 77 ++++-- projects/app/jest.config.js | 9 +- projects/app/package.json | 2 + .../components/common/folder/MoveModal.tsx | 7 +- .../components/common/folder/SlideCard.tsx | 6 +- .../permission/ConfigPerModal/index.tsx | 19 -- .../support/permission/IconText/index.tsx | 23 +- .../MemberManager/AddMemberModal.tsx | 225 ++++++++++++------ .../permission/MemberManager/ManageModal.tsx | 36 ++- .../MemberManager/MemberListCard.tsx | 12 +- .../permission/MemberManager/context.tsx | 17 +- .../components/SelectMember.tsx | 33 +-- projects/app/src/global/core/app/api.d.ts | 1 - projects/app/src/pages/api/__mocks__/base.ts | 76 ++++-- .../app/src/pages/api/__mocks__/db/init.ts | 63 ++--- .../app/src/pages/api/__mocks__/type.d.ts | 9 +- .../src/pages/api/core/app/folder/create.ts | 59 +++-- projects/app/src/pages/api/core/app/list.ts | 85 +++++-- projects/app/src/pages/api/core/app/update.ts | 191 ++++++--------- .../src/pages/api/core/dataset/allDataset.ts | 75 ++++-- .../pages/api/core/dataset/folder/create.ts | 53 +++-- .../app/src/pages/api/core/dataset/list.ts | 86 +++++-- .../app/src/pages/api/core/dataset/update.ts | 147 +++++------- .../pages/api/support/outLink/update.test.ts | 16 +- .../pages/app/detail/components/InfoModal.tsx | 57 ++--- .../detail/components/SimpleApp/AppCard.tsx | 24 +- .../pages/app/detail/components/context.tsx | 2 +- .../src/pages/app/list/components/List.tsx | 77 +++--- .../src/pages/app/list/components/context.tsx | 17 +- projects/app/src/pages/app/list/index.tsx | 47 ++-- .../pages/dataset/detail/components/Info.tsx | 72 ++---- .../src/pages/dataset/list/component/List.tsx | 102 ++++---- .../dataset/list/component/MoveModal.tsx | 186 --------------- .../app/src/pages/dataset/list/context.tsx | 17 +- projects/app/src/pages/dataset/list/index.tsx | 45 ++-- projects/app/src/test/utils.ts | 2 +- projects/app/src/web/core/app/constants.ts | 3 - .../src/web/core/dataset/api/collaborator.ts | 4 +- .../app/src/web/core/dataset/constants.ts | 3 - .../app/src/web/support/user/useUserStore.ts | 5 + 60 files changed, 1145 insertions(+), 1097 deletions(-) delete mode 100644 projects/app/src/pages/dataset/list/component/MoveModal.tsx diff --git a/docSite/content/zh-cn/docs/development/upgrading/4812.md b/docSite/content/zh-cn/docs/development/upgrading/4812.md index e058e011dbeb..ca97781b20be 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4812.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4812.md @@ -23,7 +23,9 @@ weight: 812 9. 新增 - 数据库连接和操作插件 10. 新增 - Cookie 隐私协议提示 11. 新增 - HTTP 节点支持 JSONPath 表达式 -12. 修复 - 文件后缀判断,去除 query 影响。 -13. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。 -14. 修复 - 用户交互节点未阻塞流程。 -15. 修复 - 新建 APP,有时候会导致空指针报错。 +12. 新增 - 应用和知识库支持成员组配置权限 +13. 优化 - 循环节点支持选择外部节点的变量 +14. 修复 - 文件后缀判断,去除 query 影响。 +15. 修复 - AI 响应为空时,会造成 LLM 历史记录合并。 +16. 修复 - 用户交互节点未阻塞流程。 +17. 修复 - 新建 APP,有时候会导致空指针报错。 diff --git a/packages/global/core/app/collaborator.d.ts b/packages/global/core/app/collaborator.d.ts index f9773ec94428..ca0fec7217f0 100644 --- a/packages/global/core/app/collaborator.d.ts +++ b/packages/global/core/app/collaborator.d.ts @@ -1,4 +1,8 @@ -import { UpdateClbPermissionProps } from '../../support/permission/collaborator'; +import { RequireOnlyOne } from '../../common/type/utils'; +import { + UpdateClbPermissionProps, + UpdatePermissionBody +} from '../../support/permission/collaborator'; import { PermissionValueType } from '../../support/permission/type'; export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & { @@ -7,5 +11,7 @@ export type UpdateAppCollaboratorBody = UpdateClbPermissionProps & { export type AppCollaboratorDeleteParams = { appId: string; +} & RequireOnlyOne<{ tmbId: string; -}; + groupId: string; +}>; diff --git a/packages/global/core/app/type.d.ts b/packages/global/core/app/type.d.ts index 234920e2062e..8f6cf209a73b 100644 --- a/packages/global/core/app/type.d.ts +++ b/packages/global/core/app/type.d.ts @@ -10,7 +10,6 @@ import { SelectedDatasetType } from '../workflow/api'; import { DatasetSearchModeEnum } from '../dataset/constants'; import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d'; import { StoreEdgeItemType } from '../workflow/type/edge'; -import { PermissionSchemaType, PermissionValueType } from '../../support/permission/type'; import { AppPermission } from '../../support/permission/app/controller'; import { ParentIdType } from '../../common/parentFolder/type'; import { FlowNodeInputTypeEnum } from 'core/workflow/node/constant'; @@ -45,7 +44,11 @@ export type AppSchema = { inited?: boolean; teamTags: string[]; -} & PermissionSchemaType; + inheritPermission?: boolean; + + // abandon + defaultPermission?: number; +}; export type AppListItemType = { _id: string; @@ -57,7 +60,9 @@ export type AppListItemType = { updateTime: Date; pluginData?: AppSchema['pluginData']; permission: AppPermission; -} & PermissionSchemaType; + inheritPermission?: boolean; + private?: boolean; +}; export type AppDetailType = AppSchema & { permission: AppPermission; diff --git a/packages/global/core/dataset/collaborator.d.ts b/packages/global/core/dataset/collaborator.d.ts index 543d95f29272..7f33f4d516be 100644 --- a/packages/global/core/dataset/collaborator.d.ts +++ b/packages/global/core/dataset/collaborator.d.ts @@ -1,5 +1,6 @@ import { UpdateClbPermissionProps } from '../../support/permission/collaborator'; import { PermissionValueType } from '../../support/permission/type'; +import { RequireOnlyOne } from '../../common/type/utils'; export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & { datasetId: string; @@ -7,5 +8,7 @@ export type UpdateDatasetCollaboratorBody = UpdateClbPermissionProps & { export type DatasetCollaboratorDeleteParams = { datasetId: string; +} & RequireOnlyOne<{ tmbId: string; -}; + groupId: string; +}>; diff --git a/packages/global/core/dataset/type.d.ts b/packages/global/core/dataset/type.d.ts index b2b720dc21b5..25c1fd56cc53 100644 --- a/packages/global/core/dataset/type.d.ts +++ b/packages/global/core/dataset/type.d.ts @@ -1,4 +1,3 @@ -import { PermissionSchemaType } from '../../support/permission/type'; import type { LLMModelItemType, VectorModelItemType } from '../../core/ai/model.d'; import { PermissionTypeEnum } from '../../support/permission/constant'; import { PushDatasetDataChunkProps } from './api'; @@ -32,8 +31,11 @@ export type DatasetSchemaType = { selector: string; }; externalReadUrl?: string; -} & PermissionSchemaType; -// } & PermissionSchemaType; + inheritPermission: boolean; + + // abandon + defaultPermission?: number; +}; export type DatasetCollectionSchemaType = { _id: string; @@ -146,7 +148,9 @@ export type DatasetListItemType = { type: `${DatasetTypeEnum}`; permission: DatasetPermission; vectorModel: VectorModelItemType; -} & PermissionSchemaType; + inheritPermission: boolean; + private?: boolean; +}; export type DatasetItemType = Omit & { vectorModel: VectorModelItemType; diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index ea7801446299..60a84a9d58e9 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -4,11 +4,13 @@ import { PermissionValueType } from './type'; export type CollaboratorItemType = { teamId: string; - tmbId: string; permission: Permission; name: string; avatar: string; -}; +} & RequireOnlyOne<{ + tmbId: string; + groupId: string; +}>; export type UpdateClbPermissionProps = { members?: string[]; diff --git a/packages/global/support/permission/constant.ts b/packages/global/support/permission/constant.ts index 6e0e2c1b0acf..7824dd72d396 100644 --- a/packages/global/support/permission/constant.ts +++ b/packages/global/support/permission/constant.ts @@ -1,4 +1,3 @@ -import { Permission } from './controller'; import { PermissionListType } from './type'; import { i18nT } from '../../../web/i18n/utils'; export enum AuthUserTypeEnum { diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.d.ts index a97b43c144c4..f6f29c52a863 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.d.ts @@ -1,6 +1,7 @@ import { RequireOnlyOne } from '../../common/type/utils'; import { TeamMemberWithUserSchema } from '../user/team/type'; import { AuthUserTypeEnum, PermissionKeyEnum, PerResourceTypeEnum } from './constant'; +import { MemberGroupSchemaType } from './memberGroup/type'; // PermissionValueType, the type of permission's value is a number, which is a bit field actually. // It is spired by the permission system in Linux. @@ -33,6 +34,10 @@ export type ResourcePerWithTmbWithUser = Omit & tmbId: TeamMemberWithUserSchema; }; +export type ResourcePerWithGroup = Omit & { + groupId: MemberGroupSchemaType; +}; + export type PermissionSchemaType = { defaultPermission: PermissionValueType; inheritPermission: boolean; diff --git a/packages/service/core/app/schema.ts b/packages/service/core/app/schema.ts index 0e2f117b0082..4b069cb8caaf 100644 --- a/packages/service/core/app/schema.ts +++ b/packages/service/core/app/schema.ts @@ -5,8 +5,6 @@ import { TeamCollectionName, TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; -import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; -import { getPermissionSchema } from '@fastgpt/global/support/permission/utils'; export const AppCollectionName = 'apps'; @@ -111,8 +109,13 @@ const AppSchema = new Schema({ inited: { type: Boolean }, + inheritPermission: { + type: Boolean, + default: true + }, - ...getPermissionSchema(AppDefaultPermissionVal) + // abandoned + defaultPermission: Number }); AppSchema.index({ teamId: 1, updateTime: -1 }); diff --git a/packages/service/core/dataset/schema.ts b/packages/service/core/dataset/schema.ts index 99572b58577d..c0143a39cd94 100644 --- a/packages/service/core/dataset/schema.ts +++ b/packages/service/core/dataset/schema.ts @@ -9,8 +9,6 @@ import { TeamCollectionName, TeamMemberCollectionName } from '@fastgpt/global/support/user/team/constant'; -import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; -import { getPermissionSchema } from '@fastgpt/global/support/permission/utils'; import type { DatasetSchemaType } from '@fastgpt/global/core/dataset/type.d'; export const DatasetCollectionName = 'datasets'; @@ -88,7 +86,13 @@ const DatasetSchema = new Schema({ externalReadUrl: { type: String }, - ...getPermissionSchema(DatasetDefaultPermissionVal) + inheritPermission: { + type: Boolean, + default: true + }, + + // abandoned + defaultPermission: Number }); try { diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 9a545b05ffcc..f5fc5e33b1ca 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -13,6 +13,7 @@ import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { splitCombinePluginId } from '../../../core/app/plugin/controller'; import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants'; import { AuthModeType, AuthResponseType } from '../type'; +import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; export const authPluginByTmbId = async ({ tmbId, @@ -60,7 +61,6 @@ export const authAppByTmbId = async ({ if (isRoot) { return { ...app, - defaultPermission: app.defaultPermission, permission: new AppPermission({ isOwner: true }) }; } @@ -71,7 +71,7 @@ export const authAppByTmbId = async ({ const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); - const { Per, defaultPermission } = await (async () => { + const { Per } = await (async () => { if ( AppFolderTypeList.includes(app.type) || app.inheritPermission === false || @@ -86,10 +86,9 @@ export const authAppByTmbId = async ({ resourceId: appId, resourceType: PerResourceTypeEnum.app }); - const Per = new AppPermission({ per: rp ?? app.defaultPermission, isOwner }); + const Per = new AppPermission({ per: rp ?? AppDefaultPermissionVal, isOwner }); return { - Per, - defaultPermission: app.defaultPermission + Per }; } else { // is not folder and inheritPermission is true and is not root folder. @@ -104,8 +103,7 @@ export const authAppByTmbId = async ({ isOwner }); return { - Per, - defaultPermission: parent.defaultPermission + Per }; } })(); @@ -116,7 +114,6 @@ export const authAppByTmbId = async ({ return { ...app, - defaultPermission, permission: Per }; })(); diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 3924f909ac9f..59deba454ce4 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -10,12 +10,17 @@ import { MongoResourcePermission } from './schema'; import { ClientSession } from 'mongoose'; import { PermissionValueType, - ResourcePermissionType + ResourcePermissionType, + ResourcePerWithGroup, + ResourcePerWithTmbWithUser } from '@fastgpt/global/support/permission/type'; import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { addMinutes } from 'date-fns'; import { getGroupsByTmbId } from './memberGroup/controllers'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; +import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -123,20 +128,94 @@ export async function getResourceAllClbs({ ).lean(); } +export async function getResourceClbsAndGroups({ + resourceId, + resourceType, + teamId, + session +}: { + resourceId: ParentIdType; + resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; + teamId: string; + session: ClientSession; +}) { + return MongoResourcePermission.find( + { + resourceId, + resourceType, + teamId + }, + undefined, + { session } + ).lean(); +} + +export const getClbsAndGroupsWithInfo = async ({ + resourceId, + resourceType, + teamId +}: { + resourceId: ParentIdType; + resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; + teamId: string; +}) => + Promise.all([ + (await MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + tmbId: { + $exists: true + } + }).populate({ + path: 'tmbId', + select: 'name userId', + populate: { + path: 'userId', + select: 'avatar' + } + })) as ResourcePerWithTmbWithUser[], + (await MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + groupId: { + $exists: true + } + }).populate({ + path: 'groupId', + select: 'name avatar' + })) as ResourcePerWithGroup[] + ]); + export const delResourcePermissionById = (id: string) => { return MongoResourcePermission.findByIdAndRemove(id); }; export const delResourcePermission = ({ session, + tmbId, + groupId, ...props }: { resourceType: PerResourceTypeEnum; teamId: string; resourceId: string; - tmbId: string; session?: ClientSession; + tmbId?: string; + groupId?: string; }) => { - return MongoResourcePermission.deleteOne(props, { session }); + // tmbId or groupId only one and not both + if (!!tmbId === !!groupId) { + return Promise.reject(CommonErrEnum.missingParams); + } + return MongoResourcePermission.deleteOne( + { + ...(tmbId ? { tmbId } : {}), + ...(groupId ? { groupId } : {}), + ...props + }, + { session } + ); }; /* 下面代码等迁移 */ diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 2592b02c8751..7c44b9e46f94 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -20,6 +20,7 @@ import { MongoDatasetData } from '../../../core/dataset/data/schema'; import { AuthModeType, AuthResponseType } from '../type'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; +import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; export const authDatasetByTmbId = async ({ tmbId, @@ -62,7 +63,7 @@ export const authDatasetByTmbId = async ({ const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); // get dataset permission or inherit permission from parent folder. - const { Per, defaultPermission } = await (async () => { + const { Per } = await (async () => { if ( dataset.type === DatasetTypeEnum.folder || dataset.inheritPermission === false || @@ -78,12 +79,11 @@ export const authDatasetByTmbId = async ({ resourceType: PerResourceTypeEnum.dataset }); const Per = new DatasetPermission({ - per: rp ?? dataset.defaultPermission, + per: rp ?? DatasetDefaultPermissionVal, isOwner }); return { - Per, - defaultPermission: dataset.defaultPermission + Per }; } else { // is not folder and inheritPermission is true and is not root folder. @@ -100,8 +100,7 @@ export const authDatasetByTmbId = async ({ }); return { - Per, - defaultPermission: parent.defaultPermission + Per }; } })(); @@ -112,7 +111,6 @@ export const authDatasetByTmbId = async ({ return { ...dataset, - defaultPermission, permission: Per }; })(); @@ -179,14 +177,15 @@ export async function authDatasetCollection({ tmbId, datasetId: collection.datasetId._id, per, - isRoot: isRootFromHeader || isRoot + isRoot: isRootFromHeader }); return { teamId, tmbId, collection, - permission: dataset.permission + permission: dataset.permission, + isRoot: isRootFromHeader }; } @@ -231,7 +230,8 @@ export async function authDatasetFile({ teamId, tmbId, file, - permission + permission, + isRoot }; } catch (error) { return Promise.reject(DatasetErrEnum.unAuthDatasetFile); diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index a30dfb8b91d5..c3ac42eb8a3f 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,9 +1,9 @@ import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { MongoResourcePermission } from './schema'; import { ClientSession, Model } from 'mongoose'; -import { NullPermission, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { getResourceAllClbs } from './controller'; +import { getResourceClbsAndGroups } from './controller'; import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; @@ -28,7 +28,6 @@ export async function syncChildrenPermission({ resourceModel, session, - defaultPermission, collaborators }: { resource: SyncChildrenPermissionResourceType; @@ -42,7 +41,6 @@ export async function syncChildrenPermission({ // should be provided when inheritPermission is true session: ClientSession; - defaultPermission?: PermissionValueType; collaborators?: UpdateCollaboratorItem[]; }) { // only folder has permission @@ -76,19 +74,6 @@ export async function syncChildrenPermission({ } if (!children.length) return; - // Sync default permission - if (defaultPermission !== undefined) { - await resourceModel.updateMany( - { - _id: { $in: children } - }, - { - defaultPermission - }, - { session } - ); - } - // sync the resource permission if (collaborators) { // Update the collaborators of all children @@ -124,28 +109,20 @@ export async function resumeInheritPermission({ const isFolder = folderTypeList.includes(resource.type); const fn = async (session: ClientSession) => { - const parentResource = await resourceModel - .findById(resource.parentId, 'defaultPermission') - .lean() - .session(session); - - const parentDefaultPermissionVal = parentResource?.defaultPermission ?? NullPermission; - // update the resource permission await resourceModel.updateOne( { _id: resource._id }, { - inheritPermission: true, - defaultPermission: parentDefaultPermissionVal + inheritPermission: true }, { session } ); // Folder resource, need to sync children if (isFolder) { - const parentClbs = await getResourceAllClbs({ + const parentClbsAndGroups = await getResourceClbsAndGroups({ resourceId: resource.parentId, teamId: resource.teamId, resourceType, @@ -155,7 +132,7 @@ export async function resumeInheritPermission({ // sync self await syncCollaborators({ resourceType, - collaborators: parentClbs, + collaborators: parentClbsAndGroups, teamId: resource.teamId, resourceId: resource._id, session @@ -169,8 +146,7 @@ export async function resumeInheritPermission({ folderTypeList, resourceType, session, - defaultPermission: parentDefaultPermissionVal, - collaborators: parentClbs + collaborators: parentClbsAndGroups }); } else { // Not folder, delete all clb @@ -215,6 +191,7 @@ export async function syncCollaborators({ resourceId, resourceType: resourceType, tmbId: item.tmbId, + groupId: item.groupId, permission: item.permission })), { diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 7203f4b2de98..c6027ffb9992 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -64,7 +64,7 @@ export const getGroupsByTmbId = async ({ groupId: { $exists: true }, - role: role ? { $in: role } : undefined + ...(role ? { role: { $in: role } } : {}) }) .populate('groupId') .lean() diff --git a/packages/service/support/permission/type.d.ts b/packages/service/support/permission/type.d.ts index 176903aa1379..e6ce6c7973e9 100644 --- a/packages/service/support/permission/type.d.ts +++ b/packages/service/support/permission/type.d.ts @@ -28,5 +28,6 @@ export type AuthResponseType = { authType?: `${AuthUserTypeEnum}`; appId?: string; apikey?: string; + isRoot: boolean; permission: T; }; diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index 78855b443591..4b9ae40a2f12 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -8,7 +8,7 @@ import { TeamPermission } from '@fastgpt/global/support/permission/user/controll /* auth user role */ export async function authUserPer(props: AuthModeType): Promise< - AuthResponseType & { + AuthResponseType & { tmb: TeamTmbItemType; } > { diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index d414214cad3b..e9af635d952a 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -71,6 +71,7 @@ "modules.Title is required": "模块名不能为空", "month.unit": "号", "move_app": "移动应用", + "move.hint": "移动后,所选应用/文件夹将继承新文件夹的权限设置,原先的权限设置失效。", "not_json_file": "请选择JSON文件", "or_drag_JSON": "或拖入JSON文件", "paste_config": "粘贴配置", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 4254d9549935..948b2c8d956f 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -20,6 +20,7 @@ "Folder": "文件夹", "Login": "登录", "Move": "移动", + "move.confirm": "确认移动", "Name": "名称", "None": "无", "Rename": "重命名", @@ -82,6 +83,8 @@ "code_error.team_error.un_auth": "无权操作该团队", "code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", + "code_error.team_error.group_name_duplicate": "群组名称重复", + "code_error.team_error.user_not_active": "用户未接受或已离开团队", "code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.bin_visitor": "您的身份校验未通过", @@ -915,7 +918,7 @@ "permission.Permission config": "权限配置", "permission.Private": "私有", "permission.Private Tip": "仅自己可用", - "permission.Public": "团队", + "permission.Public": "协作", "permission.Public Tip": "团队所有成员可使用", "permission.Remove InheritPermission Confirm": "此操作会导致权限继承失效,是否进行?", "permission.Resume InheritPermission Confirm": "是否恢复为继承父级文件夹的权限?", @@ -1194,7 +1197,7 @@ "user.team.invite.Reject Confirm": "确认拒绝该邀请?", "user.team.invite.accept": "接受", "user.team.invite.reject": "拒绝", - "user.team.member.Confirm Leave": "确认离开该团队?", + "user.team.member.Confirm Leave": "确认离开该团队?\n退出后,您在该团队所有的资源( 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。", "user.team.member.active": "已加入", "user.team.member.reject": "拒绝", "user.team.member.waiting": "待接受", diff --git a/packages/web/i18n/zh/dataset.json b/packages/web/i18n/zh/dataset.json index 29582f8ec32b..056ab005d704 100644 --- a/packages/web/i18n/zh/dataset.json +++ b/packages/web/i18n/zh/dataset.json @@ -34,5 +34,6 @@ "website_dataset_desc": "Web 站点同步允许你直接使用一个网页链接构建知识库", "permission.des.read": "可查看知识库内容", "permission.des.write": "可增加和变更知识库内容", - "permission.des.manage": "可管理整个知识库数据和信息" -} \ No newline at end of file + "permission.des.manage": "可管理整个知识库数据和信息", + "move.hint": "移动后,所选知识库/文件夹将继承新文件夹的权限设置,原先的权限设置失效。" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d6c2564d746..3b5de0dfb806 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,7 +560,7 @@ importers: version: 1.77.8 ts-jest: specifier: ^29.1.0 - version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0))(typescript@5.5.3) + version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3) use-context-selector: specifier: ^1.4.4 version: 1.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(scheduler@0.23.2) @@ -568,12 +568,18 @@ importers: specifier: ^4.3.5 version: 4.5.4(@types/react@18.3.1)(immer@9.0.21)(react@18.3.1) devDependencies: + '@faker-js/faker': + specifier: ^9.0.3 + version: 9.0.3 '@shelf/jest-mongodb': specifier: ^4.3.2 version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3)) '@svgr/webpack': specifier: ^6.5.1 version: 6.5.1 + '@types/faker': + specifier: ^6.6.9 + version: 6.6.9 '@types/formidable': specifier: ^2.0.5 version: 2.0.6 @@ -694,7 +700,7 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0))(typescript@5.5.3) + version: 29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3) ts-loader: specifier: ^9.4.3 version: 9.5.1(typescript@5.5.3)(webpack@5.92.1) @@ -1991,7 +1997,7 @@ packages: '@emotion/use-insertion-effect-with-fallbacks@1.0.1': resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} peerDependencies: - react: 18.3.1 + react: '>=16.8.0' '@emotion/utils@1.2.1': resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} @@ -2287,6 +2293,10 @@ packages: resolution: {integrity: sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@faker-js/faker@9.0.3': + resolution: {integrity: sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} engines: {node: '>=14'} @@ -2610,8 +2620,8 @@ packages: resolution: {integrity: sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==} peerDependencies: monaco-editor: '>= 0.25.0 < 1' - react: 18.3.1 - react-dom: 18.3.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 '@mongodb-js/saslprep@1.1.7': resolution: {integrity: sha512-dCHW/oEX0KJ4NjDULBo3JiOaK5+6axtpBbS+ao2ZInoAL9/YRQLhXzSNAFz7hP4nzLkIqsfYAK/PDE3+XHny0Q==} @@ -2962,8 +2972,8 @@ packages: '@reactflow/node-resizer@2.2.14': resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==} peerDependencies: - react: 18.3.1 - react-dom: 18.3.1 + react: '>=17' + react-dom: '>=17' '@reactflow/node-toolbar@1.3.14': resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==} @@ -3332,6 +3342,10 @@ packages: '@types/express@4.17.21': resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/faker@6.6.9': + resolution: {integrity: sha512-Y9YYm5L//8ooiiknO++4Gr539zzdI0j3aXnOBjo1Vk+kTvffY10GuE2wn78AFPECwZ5MYGTjiDVw1naLLdDimw==} + deprecated: This is a stub types definition. faker provides its own type definitions, so you do not need this installed. + '@types/formidable@2.0.6': resolution: {integrity: sha512-L4HcrA05IgQyNYJj6kItuIkXrInJvsXTPC5B1i64FggWKKqSL+4hgt7asiSNva75AoLQjq29oPxFfU4GAQ6Z2w==} @@ -5193,6 +5207,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + faker@6.6.6: + resolution: {integrity: sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==} + fast-content-type-parse@1.1.0: resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} @@ -7054,8 +7071,8 @@ packages: peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.41.2 - react: 18.3.1 - react-dom: 18.3.1 + react: ^18.2.0 + react-dom: ^18.2.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -7707,8 +7724,8 @@ packages: react-photo-view@1.2.6: resolution: {integrity: sha512-Fq17yxkMIv0oFp7HOJr39HgCZRP6A9K5T5rixJ4flSUYT2OO3V8vNxEExjhIKgIrfmTu+mDnHYEsI9RRWi1JHw==} peerDependencies: - react: 18.3.1 - react-dom: 18.3.1 + react: '>=16.8.0' + react-dom: '>=16.8.0' react-redux@7.2.9: resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} @@ -7726,8 +7743,8 @@ packages: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.1 - react: 18.3.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7746,8 +7763,8 @@ packages: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.1 - react: 18.3.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -8762,8 +8779,8 @@ packages: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.1 - react: 18.3.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -8813,8 +8830,8 @@ packages: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: - '@types/react': 18.3.1 - react: 18.3.1 + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -11121,6 +11138,8 @@ snapshots: '@eslint/js@8.56.0': {} + '@faker-js/faker@9.0.3': {} + '@fastify/accept-negotiator@1.1.0': {} '@fastify/ajv-compiler@3.6.0': @@ -12345,6 +12364,10 @@ snapshots: '@types/qs': 6.9.15 '@types/serve-static': 1.15.7 + '@types/faker@6.6.9': + dependencies: + faker: 6.6.6 + '@types/formidable@2.0.6': dependencies: '@types/node': 20.14.11 @@ -14384,7 +14407,7 @@ snapshots: eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0) eslint-plugin-react: 7.34.4(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0) @@ -14407,8 +14430,8 @@ snapshots: debug: 4.3.5 enhanced-resolve: 5.17.0 eslint: 8.56.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.14.0 @@ -14419,7 +14442,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -14430,7 +14453,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -14440,7 +14463,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) hasown: 2.0.2 is-core-module: 2.14.0 is-glob: 4.0.3 @@ -14693,6 +14716,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + faker@6.6.6: {} + fast-content-type-parse@1.1.0: {} fast-decode-uri-component@1.0.1: {} @@ -18832,7 +18857,7 @@ snapshots: ts-dedent@2.2.0: {} - ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0))(typescript@5.5.3): + ts-jest@29.2.2(@babel/core@7.24.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.9))(jest@29.7.0(@types/node@20.14.11)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.14.11)(typescript@5.5.3)))(typescript@5.5.3): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 diff --git a/projects/app/jest.config.js b/projects/app/jest.config.js index a2186c09afce..73aa436ade82 100644 --- a/projects/app/jest.config.js +++ b/projects/app/jest.config.js @@ -34,12 +34,7 @@ const config = { // coverageProvider: "babel", // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], + coverageReporters: ['json', 'text', 'lcov', 'clover'], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, @@ -163,7 +158,7 @@ const config = { // testRunner: "jest-circus/runner", // A map from regular expressions to paths to transformers - // transform: undefined, + transform: {}, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation transformIgnorePatterns: [`/node_modules/(?!${esModules})`] diff --git a/projects/app/package.json b/projects/app/package.json index b184a1440b51..6d104964e6cb 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -71,8 +71,10 @@ "zustand": "^4.3.5" }, "devDependencies": { + "@faker-js/faker": "^9.0.3", "@shelf/jest-mongodb": "^4.3.2", "@svgr/webpack": "^6.5.1", + "@types/faker": "^6.6.9", "@types/formidable": "^2.0.5", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.3", diff --git a/projects/app/src/components/common/folder/MoveModal.tsx b/projects/app/src/components/common/folder/MoveModal.tsx index 01e3f23a4b68..b89ab6bd491a 100644 --- a/projects/app/src/components/common/folder/MoveModal.tsx +++ b/projects/app/src/components/common/folder/MoveModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useState } from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; import { Box, Button, Flex, ModalBody, ModalFooter } from '@chakra-ui/react'; @@ -11,6 +11,7 @@ import { useMemoizedFn, useMount } from 'ahooks'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import LightTip from '@fastgpt/web/components/common/LightTip'; type FolderItemType = { id: string; @@ -27,9 +28,10 @@ type Props = { server: (e: GetResourceFolderListProps) => Promise; onConfirm: (id: ParentIdType) => Promise; onClose: () => void; + moveHint?: string; }; -const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) => { +const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose, moveHint }: Props) => { const { t } = useTranslation(); const [selectedId, setSelectedId] = React.useState(); const [requestingIdList, setRequestingIdList] = useState([]); @@ -170,6 +172,7 @@ const MoveModal = ({ moveResourceId, title, server, onConfirm, onClose }: Props) onClose={onClose} > + {moveHint && } diff --git a/projects/app/src/components/common/folder/SlideCard.tsx b/projects/app/src/components/common/folder/SlideCard.tsx index 1639e98fce38..16590c990bbc 100644 --- a/projects/app/src/components/common/folder/SlideCard.tsx +++ b/projects/app/src/components/common/folder/SlideCard.tsx @@ -1,5 +1,4 @@ import { Box, Button, Flex, HStack } from '@chakra-ui/react'; -import { useToast } from '@fastgpt/web/hooks/useToast'; import React from 'react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; @@ -40,7 +39,7 @@ const FolderSlideCard = ({ deleteTip: string; onDelete: () => void; - defaultPer: { + defaultPer?: { value: PermissionValueType; defaultValue: PermissionValueType; onChange: (v: PermissionValueType) => Promise; @@ -54,7 +53,6 @@ const FolderSlideCard = ({ }) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); - const { toast } = useToast(); const { ConfirmModal, openConfirm } = useConfirm({ type: 'delete', @@ -136,7 +134,7 @@ const FolderSlideCard = ({ )} - {managePer.permission.hasManagePer && ( + {managePer.permission.hasManagePer && !!defaultPer && ( {t('common:permission.Default permission')} diff --git a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx index 73cad5e19bbe..e5cd606af427 100644 --- a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx +++ b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx @@ -1,11 +1,9 @@ import React from 'react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useTranslation } from 'next-i18next'; -import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import CollaboratorContextProvider, { MemberManagerInputPropsType } from '../MemberManager/context'; import { Box, Button, Flex, HStack, ModalBody, useDisclosure } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; -import DefaultPermissionList from '../DefaultPerList'; import MyIcon from '@fastgpt/web/components/common/Icon'; import ResumeInherit from '../ResumeInheritText'; import { ChangeOwnerModal } from '../ChangeOwnerModal'; @@ -14,11 +12,6 @@ export type ConfigPerModalProps = { avatar?: string; name: string; - defaultPer: { - value: PermissionValueType; - defaultValue: PermissionValueType; - onChange: (v: PermissionValueType) => Promise; - }; managePer: MemberManagerInputPropsType; isInheritPermission?: boolean; resumeInheritPermission?: () => void; @@ -30,7 +23,6 @@ export type ConfigPerModalProps = { const ConfigPerModal = ({ avatar, name, - defaultPer, managePer, isInheritPermission, resumeInheritPermission, @@ -66,17 +58,6 @@ const ConfigPerModal = ({ )} - - {t('common:permission.Default permission')} - defaultPer.onChange(v)} - hasParent={hasParent} - /> - { const { t } = useTranslation(); - const per = useMemo(() => { - if (permission) return permission; - if (defaultPermission !== undefined) { - const Per = new Permission({ per: defaultPermission }); - if (Per.hasWritePer) return PermissionTypeEnum.publicWrite; - if (Per.hasReadPer) return PermissionTypeEnum.publicRead; - return PermissionTypeEnum.clbPrivate; - } - return 'private'; - }, [defaultPermission, permission]); + const per = Private ? 'private' : 'public'; return PermissionTypeMap[per] ? ( diff --git a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx index 439cd2e1477a..3beb58c77be7 100644 --- a/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/AddMemberModal.tsx @@ -2,12 +2,11 @@ import { Flex, Box, ModalBody, - InputGroup, - InputLeftElement, - Input, Checkbox, ModalFooter, - Button + Button, + Grid, + HStack } from '@chakra-ui/react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -18,63 +17,76 @@ import PermissionSelect from './PermissionSelect'; import PermissionTags from './PermissionTags'; import { CollaboratorContext } from './context'; import { useUserStore } from '@/web/support/user/useUserStore'; -import MyBox from '@fastgpt/web/components/common/MyBox'; import { ChevronDownIcon } from '@chakra-ui/icons'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useTranslation } from 'next-i18next'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; export type AddModalPropsType = { onClose: () => void; + mode?: 'member' | 'all'; }; -function AddMemberModal({ onClose }: AddModalPropsType) { +function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) { const { t } = useTranslation(); - const { userInfo, loadAndGetTeamMembers } = useUserStore(); + const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore(); - const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList } = + const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } = useContextSelector(CollaboratorContext, (v) => v); const [searchText, setSearchText] = useState(''); - const { data: members = [], loading: loadingMembers } = useRequest2( + const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2( async () => { - if (!userInfo?.team?.teamId) return []; - const members = await loadAndGetTeamMembers(true); - return members; + if (!userInfo?.team?.teamId) return [[], []]; + return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]); }, { manual: false, refreshDeps: [userInfo?.team?.teamId] } ); + const filterMembers = useMemo(() => { return members.filter((item) => { - // if (item.permission.isOwner) return false; if (item.tmbId === userInfo?.team?.tmbId) return false; if (!searchText) return true; return item.memberName.includes(searchText); }); }, [members, searchText, userInfo?.team?.tmbId]); + const filterGroups = useMemo(() => { + if (mode !== 'all') return []; + return groups.filter((item) => { + if (permission.isOwner) return true; // owner can see all groups + if (myGroups.find((i) => String(i._id) === String(item._id))) return false; + if (!searchText) return true; + return item.name.includes(searchText); + }); + }, [groups, searchText, myGroups, mode, permission]); + const [selectedMemberIdList, setSelectedMembers] = useState([]); + const [selectedGroupIdList, setSelectedGroupIdList] = useState([]); const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value); const perLabel = useMemo(() => { return getPerLabelList(selectedPermission).join('、'); }, [getPerLabelList, selectedPermission]); - const { mutate: onConfirm, isLoading: isUpdating } = useRequest({ - mutationFn: () => { - return onUpdateCollaborators({ + const { runAsync: onConfirm, loading: isUpdating } = useRequest2( + () => + onUpdateCollaborators({ members: selectedMemberIdList, + groups: selectedGroupIdList, permission: selectedPermission - }); - }, - successToast: t('common:common.Add Success'), - errorToast: 'Error', - onSuccess() { - onClose(); + }), + { + successToast: t('common:common.Add Success'), + errorToast: 'Error', + onSuccess() { + onClose(); + } } - }); + ); return ( - - - - - - setSearchText(e.target.value)} - /> - - + setSearchText(e.target.value)} + /> + + + {filterGroups.map((group) => { + const onChange = () => { + if (selectedGroupIdList.includes(group._id)) { + setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== group._id)); + } else { + setSelectedGroupIdList([...selectedGroupIdList, group._id]); + } + }; + const collaborator = collaboratorList.find((v) => v.groupId === group._id); + return ( + + } + /> + + + {group.name === DefaultGroupName ? userInfo?.team.teamName : group.name} + + {!!collaborator && ( + + )} + + ); + })} {filterMembers.map((member) => { const onChange = () => { if (selectedMemberIdList.includes(member.tmbId)) { @@ -123,10 +169,10 @@ function AddMemberModal({ onClose }: AddModalPropsType) { }; const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId); return ( - } - onChange={onChange} /> - - - - {member.memberName} - - {!!collaborator && ( - - )} - - + + + {member.memberName} + + {!!collaborator && ( + + )} + ); })} - {t('user:has_chosen') + ': '}+ {selectedMemberIdList.length} + {t('user:has_chosen') + ': '}{' '} + {selectedMemberIdList.length + selectedGroupIdList.length} - + + {selectedGroupIdList.map((groupId) => { + const onChange = () => { + if (selectedGroupIdList.includes(groupId)) { + setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== groupId)); + } else { + setSelectedGroupIdList([...selectedGroupIdList, groupId]); + } + }; + const group = groups.find((v) => String(v._id) === groupId); + return ( + + + + {group?.name === DefaultGroupName ? userInfo?.team.teamName : group?.name} + + + + ); + })} {selectedMemberIdList.map((tmbId) => { const member = filterMembers.find((v) => v.tmbId === tmbId); return member ? ( - + setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId)) + } > - + {member.memberName} @@ -192,16 +274,13 @@ function AddMemberModal({ onClose }: AddModalPropsType) { _hover={{ color: 'red.600' }} - onClick={() => - setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId)) - } /> - + ) : null; })} - + void; }; @@ -23,21 +24,12 @@ function ManageModal({ onClose }: ManageModalProps) { const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(CollaboratorContext, (v) => v); - const { runAsync: onDelete, loading: isDeleting } = useRequest2((tmbId: string) => - onDelOneCollaborator(tmbId) - ); + const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator); - const { runAsync: onUpdate, loading: isUpdating } = useRequest2( - ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => - onUpdateCollaborators({ - members: [tmbId], - permission: per - }), - { - successToast: t('common.Update Success'), - errorToast: 'Error' - } - ); + const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, { + successToast: t('common.Update Success'), + errorToast: 'Error' + }); const loading = isDeleting || isUpdating; @@ -74,7 +66,7 @@ function ManageModal({ onClose }: ManageModalProps) { - {item.name} + {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name} @@ -89,14 +81,18 @@ function ManageModal({ onClose }: ManageModalProps) { } value={item.permission.value} - onChange={(per) => { + onChange={(permission) => { onUpdate({ - tmbId: item.tmbId, - per + members: item.tmbId ? [item.tmbId] : undefined, + groups: item.groupId ? [item.groupId] : undefined, + permission }); }} onDelete={() => { - onDelete(item.tmbId); + onDelete({ + tmbId: item.tmbId, + groupId: item.groupId + } as RequireOnlyOne<{ tmbId: string; groupId: string }>); }} /> )} diff --git a/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx index c9de309dfb22..477c777f7f29 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberListCard.tsx @@ -6,11 +6,14 @@ import { CollaboratorContext } from './context'; import Tag, { TagProps } from '@fastgpt/web/components/common/Tag'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { useUserStore } from '@/web/support/user/useUserStore'; export type MemberListCardProps = BoxProps & { tagStyle?: Omit }; const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => { const { t } = useTranslation(); + const { userInfo } = useUserStore(); const { collaboratorList, isFetchingCollaborator } = useContextSelector( CollaboratorContext, @@ -27,10 +30,15 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => { {collaboratorList?.map((member) => { return ( - + - {member.name} + {member.name === DefaultGroupName ? userInfo?.team.teamName : member.name} ); diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index be399703bb1c..5ec06bd935af 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -15,6 +15,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useI18n } from '@/web/context/I18n'; +import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; const AddMemberModal = dynamic(() => import('./AddMemberModal')); const ManageModal = dynamic(() => import('./ManageModal')); @@ -22,10 +23,12 @@ export type MemberManagerInputPropsType = { permission: Permission; onGetCollaboratorList: () => Promise; permissionList: PermissionListType; - onUpdateCollaborators: (props: any) => any; // TODO: type. should be UpdatePermissionBody after app and dataset permission refactored - onDelOneCollaborator: (tmbId: string) => any; + onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; + onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise; refreshDeps?: any[]; + mode?: 'member' | 'all'; }; + export type MemberManagerPropsType = MemberManagerInputPropsType & { collaboratorList: CollaboratorItemType[]; refetchCollaboratorList: () => void; @@ -72,7 +75,8 @@ const CollaboratorContextProvider = ({ refetchResource, refreshDeps = [], isInheritPermission, - hasParent + hasParent, + mode = 'member' }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; refetchResource?: () => void; @@ -83,8 +87,10 @@ const CollaboratorContextProvider = ({ await onUpdateCollaborators(props); refetchCollaboratorList(); }; - const onDelOneCollaboratorThen = async (tmbId: string) => { - await onDelOneCollaborator(tmbId); + const onDelOneCollaboratorThen = async ( + props: RequireOnlyOne<{ tmbId: string; groupId: string }> + ) => { + await onDelOneCollaborator(props); refetchCollaboratorList(); }; @@ -197,6 +203,7 @@ const CollaboratorContextProvider = ({ onCloseAddMember(); refetchResource?.(); }} + mode={mode} /> )} {isOpenManageModal && ( diff --git a/projects/app/src/components/support/user/team/TeamManageModal/components/SelectMember.tsx b/projects/app/src/components/support/user/team/TeamManageModal/components/SelectMember.tsx index f2badaeaa466..57f4e2fa4aba 100644 --- a/projects/app/src/components/support/user/team/TeamManageModal/components/SelectMember.tsx +++ b/projects/app/src/components/support/user/team/TeamManageModal/components/SelectMember.tsx @@ -1,14 +1,5 @@ import React, { useMemo, useState } from 'react'; -import { - Box, - Checkbox, - Flex, - Grid, - HStack, - Input, - InputGroup, - InputLeftElement -} from '@chakra-ui/react'; +import { Box, Checkbox, Flex, Grid, HStack } from '@chakra-ui/react'; import MyIcon from '@fastgpt/web/components/common/Icon'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; @@ -16,6 +7,7 @@ import { Control, Controller } from 'react-hook-form'; import { RequireAtLeastOne } from '@fastgpt/global/common/type/utils'; import { useUserStore } from '@/web/support/user/useUserStore'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; type memberType = { type: 'member'; @@ -120,19 +112,14 @@ function SelectMember({ h={'100%'} > - - - - - { - setSearchKey(e.target.value); - }} - /> - + { + setSearchKey(e.target.value); + }} + /> {filtered.map((member) => { return ( diff --git a/projects/app/src/global/core/app/api.d.ts b/projects/app/src/global/core/app/api.d.ts index 9da2b7368f22..fe6923ff1ec2 100644 --- a/projects/app/src/global/core/app/api.d.ts +++ b/projects/app/src/global/core/app/api.d.ts @@ -12,7 +12,6 @@ export type AppUpdateParams = { edges?: AppSchema['edges']; chatConfig?: AppSchema['chatConfig']; teamTags?: AppSchema['teamTags']; - defaultPermission?: AppSchema['defaultPermission']; }; export type PostPublishAppProps = { diff --git a/projects/app/src/pages/api/__mocks__/base.ts b/projects/app/src/pages/api/__mocks__/base.ts index 261704acd7cf..74fd415409c0 100644 --- a/projects/app/src/pages/api/__mocks__/base.ts +++ b/projects/app/src/pages/api/__mocks__/base.ts @@ -1,7 +1,8 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; +import { MongoMemoryReplSet } from 'mongodb-memory-server'; import mongoose from 'mongoose'; -import { MockParseHeaderCert } from '@/test/utils'; -import { initMockData } from './db/init'; +import { parseHeaderCertMock } from '@/test/utils'; +import { initMockData, root } from './db/init'; +import { faker } from '@faker-js/faker/locale/zh_CN'; jest.mock('nanoid', () => { return { @@ -13,24 +14,40 @@ jest.mock('@fastgpt/global/common/string/tools', () => { return { hashStr(str: string) { return str; + }, + getNanoid() { + return faker.string.alphanumeric(12); } }; }); -jest.mock('@fastgpt/service/common/system/log', jest.fn()); +jest.mock('@fastgpt/service/common/system/log', () => ({ + addLog: { + log: jest.fn(), + warn: jest.fn((...prop) => { + console.warn(prop); + }), + error: jest.fn((...prop) => { + console.error(prop); + }), + info: jest.fn(), + debug: jest.fn() + } +})); -jest.mock('@fastgpt/service/support/permission/controller', () => { - return { - parseHeaderCert: MockParseHeaderCert, - getResourcePermission: jest.requireActual('@fastgpt/service/support/permission/controller') - .getResourcePermission, - getResourceAllClbs: jest.requireActual('@fastgpt/service/support/permission/controller') - .getResourceAllClbs - }; -}); +jest.setMock( + '@fastgpt/service/support/permission/controller', + (() => { + const origin = jest.requireActual< + typeof import('@fastgpt/service/support/permission/controller') + >('@fastgpt/service/support/permission/controller'); -const parse = jest.createMockFromModule('@fastgpt/service/support/permission/controller') as any; -parse.parseHeaderCert = MockParseHeaderCert; + return { + ...origin, + parseHeaderCert: parseHeaderCertMock + }; + })() +); jest.mock('@/service/middleware/entry', () => { return { @@ -59,11 +76,30 @@ jest.mock('@/service/middleware/entry', () => { beforeAll(async () => { // 新建一个内存数据库,然后让 mongoose 连接这个数据库 if (!global.mongod || !global.mongodb) { - const mongod = await MongoMemoryServer.create(); - global.mongod = mongod; + const replSet = new MongoMemoryReplSet({ + instanceOpts: [ + { + storageEngine: 'wiredTiger' + }, + { + storageEngine: 'wiredTiger' + } + ] + }); + replSet.start(); + await replSet.waitUntilRunning(); + const uri = replSet.getUri(); + // const mongod = await MongoMemoryServer.create({ + // instance: { + // replSet: 'testset' + // } + // }); + // global.mongod = mongod; + global.replSet = replSet; global.mongodb = mongoose; - await global.mongodb.connect(mongod.getUri(), { + await global.mongodb.connect(uri, { + dbName: 'fastgpt_test', bufferCommands: true, maxConnecting: 50, maxPoolSize: 50, @@ -77,6 +113,7 @@ beforeAll(async () => { }); await initMockData(); + console.log(root); } }); @@ -84,6 +121,9 @@ afterAll(async () => { if (global.mongodb) { await global.mongodb.disconnect(); } + if (global.replSet) { + await global.replSet.stop(); + } if (global.mongod) { await global.mongod.stop(); } diff --git a/projects/app/src/pages/api/__mocks__/db/init.ts b/projects/app/src/pages/api/__mocks__/db/init.ts index 804323cefd56..33514c02e099 100644 --- a/projects/app/src/pages/api/__mocks__/db/init.ts +++ b/projects/app/src/pages/api/__mocks__/db/init.ts @@ -1,5 +1,6 @@ -import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema'; import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema'; @@ -13,37 +14,43 @@ export const root = { }; export const initMockData = async () => { - const initRootUser = async () => { - // init root user - const rootUser = await MongoUser.create({ + const [rootUser] = await MongoUser.create([ + { username: 'root', password: '123456' - }); - - const rootTeam = await MongoTeam.create({ - name: 'root-default-team', - ownerId: rootUser._id - }); - - const rootTeamMember = await MongoTeamMember.create({ + } + ]); + root.uid = String(rootUser._id); + const [rootTeam] = await MongoTeam.create([ + { + name: 'root Team' + } + ]); + root.teamId = String(rootTeam._id); + const [rootTmb] = await MongoTeamMember.create([ + { teamId: rootTeam._id, + name: 'owner', + role: 'owner', userId: rootUser._id, - name: 'root-default-team-member', - status: 'active', - role: TeamMemberRoleEnum.owner - }); - const rootApp = await MongoApp.create({ - name: 'root-default-app', - teamId: rootTeam._id, - tmbId: rootTeam._id, - type: 'advanced' - }); + status: 'active' + } + ]); + root.tmbId = String(rootTmb._id); + await MongoMemberGroupModel.create([ + { + name: DefaultGroupName, + teamId: rootTeam._id + } + ]); - root.uid = rootUser._id; - root.tmbId = rootTeamMember._id; - root.teamId = rootTeam._id; - root.appId = rootApp._id; - }; + const [rootApp] = await MongoApp.create([ + { + name: 'root Test App', + teamId: rootTeam._id, + tmbId: rootTmb._id + } + ]); - await initRootUser(); + root.appId = String(rootApp._id); }; diff --git a/projects/app/src/pages/api/__mocks__/type.d.ts b/projects/app/src/pages/api/__mocks__/type.d.ts index 9f58349baf72..fe65eb341d03 100644 --- a/projects/app/src/pages/api/__mocks__/type.d.ts +++ b/projects/app/src/pages/api/__mocks__/type.d.ts @@ -1,4 +1,11 @@ -import { MongoMemoryServer } from 'mongodb-memory-server'; +import type { MongoMemoryReplSet, MongoMemoryServer } from 'mongodb-memory-server'; declare global { var mongod: MongoMemoryServer | undefined; + var replSet: MongoMemoryReplSet | undefined; } + +export type RequestResponse = { + code: number; + error?: string; + data?: T; +}; diff --git a/projects/app/src/pages/api/core/app/folder/create.ts b/projects/app/src/pages/api/core/app/folder/create.ts index cc7092f2a2bc..eb05ee1b4e20 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -2,6 +2,7 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { + OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -11,11 +12,12 @@ import { NextAPI } from '@/service/middleware/entry'; import { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/constant'; 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 { getResourceAllClbs } from '@fastgpt/service/support/permission/controller'; +import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export type CreateAppFolderBody = { parentId?: ParentIdType; @@ -31,20 +33,21 @@ async function handler(req: ApiRequestProps) { } // 凭证校验 - const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal }); - const parentApp = await (async () => { - if (parentId) { - // if it is not a root folder - return ( - await authApp({ - req, - appId: parentId, - per: WritePermissionVal, - authToken: true - }) - ).app; // check the parent folder permission - } - })(); + 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 + }); + } // Create app await mongoSessionRun(async (session) => { @@ -55,13 +58,11 @@ async function handler(req: ApiRequestProps) { intro, teamId, tmbId, - type: AppTypeEnum.folder, - // inheritPermission: !!parentApp ? true : false, - defaultPermission: !!parentApp ? parentApp.defaultPermission : AppDefaultPermissionVal + type: AppTypeEnum.folder }); if (parentId) { - const parentClbs = await getResourceAllClbs({ + const parentClbsAndGroups = await getResourceClbsAndGroups({ teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.app, @@ -72,9 +73,25 @@ async function handler(req: ApiRequestProps) { resourceType: PerResourceTypeEnum.app, teamId, resourceId: app._id, - collaborators: parentClbs, + collaborators: parentClbsAndGroups, session }); + } else { + // Create default permission + await MongoResourcePermission.create( + [ + { + resourceType: PerResourceTypeEnum.app, + teamId, + resourceId: app._id, + tmbId, + permission: OwnerPermissionVal + } + ], + { + session + } + ); } }); } diff --git a/projects/app/src/pages/api/core/app/list.ts b/projects/app/src/pages/api/core/app/list.ts index 179b3ffb5159..aadd0d4c4223 100644 --- a/projects/app/src/pages/api/core/app/list.ts +++ b/projects/app/src/pages/api/core/app/list.ts @@ -15,6 +15,8 @@ import { AppDefaultPermissionVal } from '@fastgpt/global/support/permission/app/ import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { replaceRegChars } from '@fastgpt/global/common/string/tools'; +import { getGroupPer } from '@fastgpt/service/support/permission/controller'; +import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers'; export type ListAppBody = { parentId?: ParentIdType; @@ -31,7 +33,7 @@ async function handler(req: ApiRequestProps): Promise { if (parentId) { return await authApp({ @@ -87,10 +89,17 @@ async function handler(req: ApiRequestProps): Promise String(item._id)); + + const [myApps, perList] = await Promise.all([ MongoApp.find( findAppsQuery, - '_id parentId avatar type name intro tmbId updateTime pluginData defaultPermission inheritPermission' + '_id parentId avatar type name intro tmbId updateTime pluginData inheritPermission' ) .sort({ updateTime: -1 @@ -98,41 +107,67 @@ async function handler(req: ApiRequestProps): Promise { - const Per = (() => { + const { Per, privateApp } = (() => { // Inherit app if (app.inheritPermission && ParentApp && !AppFolderTypeList.includes(app.type)) { - // get its parent's permission as its permission - app.defaultPermission = ParentApp.defaultPermission; - const perVal = rpList.find( - (item) => String(item.resourceId) === String(ParentApp._id) + const tmbPer = perList.find( + (item) => String(item.resourceId) === String(ParentApp._id) && !!item.tmbId )?.permission; + const groupPer = getGroupPer( + perList + .filter( + (item) => + String(item.resourceId) === String(ParentApp._id) && + myGroupIds.includes(String(item.groupId)) + ) + .map((item) => item.permission) + ); - return new AppPermission({ - per: perVal ?? app.defaultPermission, - isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner - }); + return { + Per: new AppPermission({ + per: tmbPer ?? groupPer ?? AppDefaultPermissionVal, + isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner + }), + privateApp: !tmbPer && !groupPer + }; } else { - const perVal = rpList.find( - (item) => String(item.resourceId) === String(app._id) + const tmbPer = perList.find( + (item) => String(item.resourceId) === String(app._id) && !!item.tmbId )?.permission; - return new AppPermission({ - per: perVal ?? app.defaultPermission, - isOwner: String(app.tmbId) === String(tmbId) || tmbPer.isOwner - }); + const group = perList.filter( + (item) => + String(item.resourceId) === String(app._id) && + myGroupIds.includes(String(item.groupId)) + ); + const groupPer = getGroupPer(group.map((item) => item.permission)); + return { + Per: new AppPermission({ + per: tmbPer ?? groupPer ?? AppDefaultPermissionVal, + isOwner: String(app.tmbId) === String(tmbId) || myPer.isOwner + }), + privateApp: !tmbPer && !groupPer + }; } })(); - return { ...app, - permission: Per + permission: Per, + privateApp: privateApp }; }) .filter((app) => app.permission.hasReadPer); @@ -148,9 +183,9 @@ async function handler(req: ApiRequestProps): Promise) { - const { - parentId, - name, - avatar, - type, - intro, - nodes, - edges, - chatConfig, - teamTags, - defaultPermission - } = req.body as AppUpdateParams; - - const { appId } = req.query as { appId: string }; +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 { AppErrEnum } from '@fastgpt/global/common/error/code/app'; + +export type AppUpdateQuery = { + appId: string; +}; + +export type AppUpdateBody = AppUpdateParams; + +// 更新应用接口 +// 包括如下功能: +// 1. 更新应用的信息(包括名称,类型,头像,介绍等) +// 2. 更新应用的编排信息 +// 3. 移动应用 +// 操作权限: +// 1. 更新信息和工作流编排需要有应用的写权限 +// 2. 移动应用需要有 +// (1) 父目录的管理权限 +// (2) 目标目录的管理权限 +// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限 +async function handler(req: ApiRequestProps) { + const { parentId, name, avatar, type, intro, nodes, edges, chatConfig, teamTags } = req.body; + + const { appId } = req.query; if (!appId) { Promise.reject(CommonErrEnum.missingParams); } + const isMove = parentId !== undefined; + + // this step is to get the app and its permission, and we will check the permission manually for + // different cases + const { app, permission } = await authApp({ + req, + authToken: true, + appId, + per: ReadPermissionVal + }); + + if (!app) { + Promise.reject(AppErrEnum.unExist); + } - const { app } = await (async () => { - if (defaultPermission !== undefined) { - // if defaultPermission or inheritPermission is set, then need manage permission - return authApp({ req, authToken: true, appId, per: ManagePermissionVal }); - } else { - return authApp({ req, authToken: true, appId, per: WritePermissionVal }); + if (isMove) { + if (parentId) { + // move to a folder, check the target folder's permission + await authApp({ req, authToken: true, appId: parentId, per: ManagePermissionVal }); } - })(); - - // format nodes data - // 1. dataset search limit, less than model quoteMaxToken - const isDefaultPermissionChanged = - defaultPermission !== undefined && defaultPermission !== app.defaultPermission; - const isFolder = AppFolderTypeList.includes(app.type); + if (app.parentId) { + // move from a folder, check the (old) folder's permission + await authApp({ req, authToken: true, appId: app.parentId, per: ManagePermissionVal }); + } + if (parentId === null || !app.parentId) { + // move to root or move from root + await authUserPer({ + req, + authToken: true, + per: TeamWritePermissionVal + }); + } + } else { + // is not move, write permission of the app. + if (!permission.hasWritePer) { + return Promise.reject(AppErrEnum.unAuthApp); + } + } - const onUpdate = async ( - session?: ClientSession, - updatedDefaultPermission?: PermissionValueType - ) => { + const onUpdate = async (session?: ClientSession) => { + // format nodes data + // 1. dataset search limit, less than model quoteMaxToken const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes }); return MongoApp.findByIdAndUpdate( @@ -84,12 +101,6 @@ async function handler(req: ApiRequestProps) ...(type && { type }), ...(avatar && { avatar }), ...(intro !== undefined && { intro }), - // update default permission(Maybe move update) - ...(updatedDefaultPermission !== undefined && { - defaultPermission: updatedDefaultPermission - }), - // Not root, update default permission - ...(app.parentId && isDefaultPermissionChanged && { inheritPermission: false }), ...(teamTags && { teamTags }), ...(formatNodes && { modules: formatNodes @@ -97,34 +108,19 @@ async function handler(req: ApiRequestProps) ...(edges && { edges }), - ...(chatConfig && { chatConfig }) + ...(chatConfig && { chatConfig }), + ...(isMove && { inheritPermission: true }) }, { session } ); }; // Move - if (parentId !== undefined) { + if (isMove) { await mongoSessionRun(async (session) => { - // Auth - const parentDefaultPermission = await (async () => { - if (parentId) { - const { app: parentApp } = await authApp({ - req, - authToken: true, - appId: parentId, - per: WritePermissionVal - }); - - return parentApp.defaultPermission; - } - - return AppDefaultPermissionVal; - })(); - // Inherit folder: Sync children permission and it's clbs - if (isFolder && app.inheritPermission) { - const parentClbs = await getResourceAllClbs({ + if (AppFolderTypeList.includes(app.type)) { + const parentClbsAndGroups = await getResourceClbsAndGroups({ teamId: app.teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.app, @@ -134,7 +130,7 @@ async function handler(req: ApiRequestProps) await syncCollaborators({ resourceId: app._id, resourceType: PerResourceTypeEnum.app, - collaborators: parentClbs, + collaborators: parentClbsAndGroups, session, teamId: app.teamId }); @@ -144,53 +140,12 @@ async function handler(req: ApiRequestProps) resourceType: PerResourceTypeEnum.app, resourceModel: MongoApp, folderTypeList: AppFolderTypeList, - defaultPermission: parentDefaultPermission, - collaborators: parentClbs, + collaborators: parentClbsAndGroups, session }); - - return onUpdate(session, parentDefaultPermission); } - return onUpdate(session); }); - } else if (isDefaultPermissionChanged) { - // Update default permission - await mongoSessionRun(async (session) => { - if (isFolder) { - // Sync children default permission - await syncChildrenPermission({ - resource: { - _id: app._id, - type: app.type, - teamId: app.teamId, - parentId: app.parentId - }, - folderTypeList: AppFolderTypeList, - resourceModel: MongoApp, - resourceType: PerResourceTypeEnum.app, - session, - defaultPermission - }); - } else if (app.inheritPermission && app.parentId) { - // Inherit app - const parentClbs = await getResourceAllClbs({ - teamId: app.teamId, - resourceId: app.parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - await syncCollaborators({ - resourceId: app._id, - resourceType: PerResourceTypeEnum.app, - collaborators: parentClbs, - session, - teamId: app.teamId - }); - } - - return onUpdate(session, defaultPermission); - }); } else { return onUpdate(); } diff --git a/projects/app/src/pages/api/core/dataset/allDataset.ts b/projects/app/src/pages/api/core/dataset/allDataset.ts index f3ac48e33b6d..daacf43ba0bd 100644 --- a/projects/app/src/pages/api/core/dataset/allDataset.ts +++ b/projects/app/src/pages/api/core/dataset/allDataset.ts @@ -2,7 +2,6 @@ import type { NextApiRequest } from 'next'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; import { getVectorModel } from '@fastgpt/service/core/ai/model'; import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d'; -import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { NextAPI } from '@/service/middleware/entry'; import { PerResourceTypeEnum, @@ -11,6 +10,9 @@ import { import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; +import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; +import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers'; +import { getGroupPer } from '@fastgpt/service/support/permission/controller'; /* get all dataset by teamId or tmbId */ async function handler(req: NextApiRequest): Promise { @@ -25,7 +27,14 @@ async function handler(req: NextApiRequest): Promise { per: ReadPermissionVal }); - const [myDatasets, rpList] = await Promise.all([ + const myGroupIds = ( + await getGroupsByTmbId({ + tmbId, + teamId + }) + ).map((item) => String(item._id)); + + const [myDatasets, perList] = await Promise.all([ MongoDataset.find({ teamId }) @@ -34,39 +43,59 @@ async function handler(req: NextApiRequest): Promise { }) .lean(), MongoResourcePermission.find({ - resourceType: PerResourceTypeEnum.dataset, - teamId, - tmbId + $and: [ + { + resourceType: PerResourceTypeEnum.dataset, + teamId, + resourceId: { + $exists: true + } + }, + { $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] } + ] }).lean() ]); const filterDatasets = myDatasets .map((dataset) => { const perVal = (() => { - const perVal = rpList.find( - (item) => String(item.resourceId) === String(dataset._id) - )?.permission; - if (perVal) { - return perVal; - } + const parentDataset = myDatasets.find( + (item) => String(item._id) === String(dataset.parentId) + ); - if (dataset.inheritPermission && dataset.parentId) { - const parentDataset = myDatasets.find( - (item) => String(item._id) === String(dataset.parentId) + if (dataset.inheritPermission && dataset.parentId && parentDataset) { + const tmbPer = perList.find( + (item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId + )?.permission; + const groupPer = getGroupPer( + perList + .filter( + (item) => + String(item.resourceId) === String(parentDataset._id) && + myGroupIds.includes(String(item.groupId)) + ) + .map((item) => item.permission) ); - if (parentDataset) { - const parentPerVal = - rpList.find((item) => String(item.resourceId) === String(parentDataset._id)) - ?.permission ?? parentDataset.defaultPermission; - if (parentPerVal) { - return parentPerVal; - } - } + return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal; + } else { + const tmbPer = perList.find( + (item) => String(item.resourceId) === String(dataset._id) && !!item.tmbId + )?.permission; + const groupPer = getGroupPer( + perList + .filter( + (item) => + String(item.resourceId) === String(dataset._id) && + myGroupIds.includes(String(item.groupId)) + ) + .map((item) => item.permission) + ); + return tmbPer ?? groupPer ?? DatasetDefaultPermissionVal; } })(); const Per = new DatasetPermission({ - per: perVal ?? dataset.defaultPermission, + per: perVal ?? DatasetDefaultPermissionVal, isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner }); diff --git a/projects/app/src/pages/api/core/dataset/folder/create.ts b/projects/app/src/pages/api/core/dataset/folder/create.ts index f6ca7d16a4e7..5f9a7de2671b 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -4,6 +4,7 @@ import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { + OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -12,9 +13,9 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; -import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller'; +import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export type DatasetFolderCreateQuery = {}; export type DatasetFolderCreateBody = { parentId?: string; @@ -38,35 +39,28 @@ async function handler( authToken: true }); - const parentFolder = await (async () => { - if (parentId) { - return ( - await authDataset({ - datasetId: parentId, - per: WritePermissionVal, - req, - authToken: true - }) - ).dataset; - } - })(); + if (parentId) { + await authDataset({ + datasetId: parentId, + per: WritePermissionVal, + req, + authToken: true + }); + } await mongoSessionRun(async (session) => { - const app = await MongoDataset.create({ + const dataset = await MongoDataset.create({ ...parseParentIdInMongo(parentId), avatar: FolderImgUrl, name, intro, teamId, tmbId, - type: DatasetTypeEnum.folder, - defaultPermission: !!parentFolder - ? parentFolder.defaultPermission - : DatasetDefaultPermissionVal + type: DatasetTypeEnum.folder }); if (parentId) { - const parentClbs = await getResourceAllClbs({ + const parentClbsAndGroups = await getResourceClbsAndGroups({ teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.dataset, @@ -76,11 +70,26 @@ async function handler( await syncCollaborators({ resourceType: PerResourceTypeEnum.dataset, teamId, - resourceId: app._id, - collaborators: parentClbs, + resourceId: dataset._id, + collaborators: parentClbsAndGroups, session }); } + + if (!parentId) { + await MongoResourcePermission.create( + [ + { + resourceType: PerResourceTypeEnum.dataset, + teamId, + resourceId: dataset._id, + tmbId, + permission: OwnerPermissionVal + } + ], + { session } + ); + } }); return {}; diff --git a/projects/app/src/pages/api/core/dataset/list.ts b/projects/app/src/pages/api/core/dataset/list.ts index d513063e4009..7560abd23694 100644 --- a/projects/app/src/pages/api/core/dataset/list.ts +++ b/projects/app/src/pages/api/core/dataset/list.ts @@ -16,6 +16,8 @@ import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils' import { ApiRequestProps } from '@fastgpt/service/type/next'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { replaceRegChars } from '@fastgpt/global/common/string/tools'; +import { getGroupsByTmbId } from '@fastgpt/service/support/permission/memberGroup/controllers'; +import { getGroupPer } from '@fastgpt/service/support/permission/controller'; export type GetDatasetListBody = { parentId: ParentIdType; @@ -30,7 +32,7 @@ async function handler(req: ApiRequestProps) { dataset: parentDataset, teamId, tmbId, - permission: tmbPer + permission: myPer } = await (async () => { if (parentId) { return await authDataset({ @@ -76,44 +78,84 @@ async function handler(req: ApiRequestProps) { }; })(); - const [myDatasets, rpList] = await Promise.all([ + const myGroupIds = ( + await getGroupsByTmbId({ + tmbId, + teamId + }) + ).map((item) => String(item._id)); + + const [myDatasets, perList] = await Promise.all([ MongoDataset.find(findDatasetQuery) .sort({ updateTime: -1 }) .lean(), MongoResourcePermission.find({ - resourceType: PerResourceTypeEnum.dataset, - teamId, - tmbId + $and: [ + { + resourceType: PerResourceTypeEnum.dataset, + teamId, + resourceId: { + $exists: true + } + }, + { $or: [{ tmbId }, { groupId: { $in: myGroupIds } }] } + ] }).lean() ]); const filterDatasets = myDatasets .map((dataset) => { - const Per = (() => { + const { Per, privateDataset } = (() => { + // inherit if (dataset.inheritPermission && parentDataset && dataset.type !== DatasetTypeEnum.folder) { - dataset.defaultPermission = parentDataset.defaultPermission; - const perVal = rpList.find( - (item) => String(item.resourceId) === String(parentDataset._id) + const tmbPer = perList.find( + (item) => String(item.resourceId) === String(parentDataset._id) && !!item.tmbId )?.permission; - return new DatasetPermission({ - per: perVal ?? parentDataset.defaultPermission, - isOwner: String(parentDataset.tmbId) === tmbId || tmbPer.isOwner - }); + const groupPer = getGroupPer( + perList + .filter( + (item) => + String(item.resourceId) === String(parentDataset._id) && + myGroupIds.includes(String(item.groupId)) + ) + .map((item) => item.permission) + ); + return { + Per: new DatasetPermission({ + per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal, + isOwner: String(parentDataset.tmbId) === tmbId || myPer.isOwner + }), + privateDataset: !tmbPer && !groupPer + }; } else { - const perVal = rpList.find( - (item) => String(item.resourceId) === String(dataset._id) + const tmbPer = perList.find( + (item) => + String(item.resourceId) === String(dataset._id) && !!item.tmbId && !!item.permission )?.permission; - return new DatasetPermission({ - per: perVal ?? dataset.defaultPermission, - isOwner: String(dataset.tmbId) === tmbId || tmbPer.isOwner - }); + const groupPer = getGroupPer( + perList + .filter( + (item) => + String(item.resourceId) === String(dataset._id) && + myGroupIds.includes(String(item.groupId)) + ) + .map((item) => item.permission) + ); + return { + Per: new DatasetPermission({ + per: tmbPer ?? groupPer ?? DatasetDefaultPermissionVal, + isOwner: String(dataset.tmbId) === tmbId || myPer.isOwner + }), + privateDataset: !tmbPer && !groupPer + }; } })(); return { ...dataset, - permission: Per + permission: Per, + privateDataset }; }) .filter((app) => app.permission.hasReadPer); @@ -127,10 +169,10 @@ async function handler(req: ApiRequestProps) { type: item.type, permission: item.permission, vectorModel: getVectorModel(item.vectorModel), - defaultPermission: item.defaultPermission ?? DatasetDefaultPermissionVal, inheritPermission: item.inheritPermission, tmbId: item.tmbId, - updateTime: item.updateTime + updateTime: item.updateTime, + private: item.privateDataset })) ); diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index bcc197d0b963..26b0c6dcb8da 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -5,63 +5,86 @@ import { NextAPI } from '@/service/middleware/entry'; import { ManagePermissionVal, PerResourceTypeEnum, - WritePermissionVal + ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { ClientSession } from 'mongoose'; -import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; -import { DatasetSchemaType } from '@fastgpt/global/core/dataset/type'; -import { getResourceAllClbs } from '@fastgpt/service/support/permission/controller'; +import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncChildrenPermission, 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 { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; +// 更新知识库接口 +// 包括如下功能: +// 1. 更新应用的信息(包括名称,类型,头像,介绍等) +// 2. 更新数据库的配置信息 +// 3. 移动知识库 +// 操作权限: +// 1. 更新信息和配置编排需要有知识库的写权限 +// 2. 移动应用需要有 +// (1) 父目录的管理权限 +// (2) 目标目录的管理权限 +// (3) 如果从根目录移动或移动到根目录,需要有团队的应用创建权限 async function handler( req: ApiRequestProps, _res: ApiResponseType ): Promise { - const { - id, - parentId, - name, - avatar, - intro, - agentModel, - websiteConfig, - externalReadUrl, - defaultPermission, - status - } = req.body; + const { id, parentId, name, avatar, intro, agentModel, websiteConfig, externalReadUrl, status } = + req.body; if (!id) { return Promise.reject(CommonErrEnum.missingParams); } - const { dataset } = (await (async () => { - if (defaultPermission !== undefined) { - return await authDataset({ req, authToken: true, datasetId: id, per: ManagePermissionVal }); - } else { - return await authDataset({ req, authToken: true, datasetId: id, per: WritePermissionVal }); + const isMove = parentId !== undefined; + + const { dataset, permission } = await authDataset({ + req, + authToken: true, + datasetId: id, + per: ReadPermissionVal + }); + if (isMove) { + if (parentId) { + // move to a folder, check the target folder's permission + await authDataset({ req, authToken: true, datasetId: parentId, per: ManagePermissionVal }); + } + if (dataset.parentId) { + // move from a folder, check the (old) folder's permission + await authDataset({ + req, + authToken: true, + datasetId: dataset.parentId, + per: ManagePermissionVal + }); } - })()) as { dataset: DatasetSchemaType }; + if (parentId === null || !dataset.parentId) { + // move to root or move from root + await authUserPer({ + req, + authToken: true, + per: TeamWritePermissionVal + }); + } + } else { + // is not move + if (!permission.hasWritePer) return Promise.reject(DatasetErrEnum.unAuthDataset); + } - const isDefaultPermissionChanged = - defaultPermission !== undefined && dataset.defaultPermission !== defaultPermission; const isFolder = dataset.type === DatasetTypeEnum.folder; - const onUpdate = async ( - session?: ClientSession, - updatedDefaultPermission?: PermissionValueType - ) => { + const onUpdate = async (session?: ClientSession) => { await MongoDataset.findByIdAndUpdate( id, { @@ -73,35 +96,16 @@ async function handler( ...(status && { status }), ...(intro !== undefined && { intro }), ...(externalReadUrl !== undefined && { externalReadUrl }), - // move - ...(updatedDefaultPermission !== undefined && { - defaultPermission: updatedDefaultPermission - }), - // update the defaultPermission - ...(dataset.parentId && isDefaultPermissionChanged && { inheritPermission: false }) + ...(isMove && { inheritPermission: true }) }, { session } ); }; - // move - if (parentId !== undefined) { + if (isMove) { await mongoSessionRun(async (session) => { - const parentDefaultPermission = await (async () => { - if (parentId) { - const { dataset: parentDataset } = await authDataset({ - req, - authToken: true, - datasetId: parentId, - per: WritePermissionVal - }); - return parentDataset.defaultPermission; - } - return DatasetDefaultPermissionVal; - })(); - if (isFolder && dataset.inheritPermission) { - const parentClbs = await getResourceAllClbs({ + const parentClbsAndGroups = await getResourceClbsAndGroups({ teamId: dataset.teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.dataset, @@ -112,7 +116,7 @@ async function handler( teamId: dataset.teamId, resourceId: id, resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbs, + collaborators: parentClbsAndGroups, session }); @@ -121,48 +125,13 @@ async function handler( resourceType: PerResourceTypeEnum.dataset, resourceModel: MongoDataset, folderTypeList: [DatasetTypeEnum.folder], - collaborators: parentClbs, - defaultPermission: parentDefaultPermission, + collaborators: parentClbsAndGroups, session }); - return onUpdate(session, parentDefaultPermission); + return onUpdate(session); } return onUpdate(session); }); - } else if (isDefaultPermissionChanged) { - await mongoSessionRun(async (session) => { - if (isFolder) { - await syncChildrenPermission({ - defaultPermission, - resource: { - _id: dataset._id, - type: dataset.type, - teamId: dataset.teamId, - parentId: dataset.parentId - }, - resourceType: PerResourceTypeEnum.dataset, - resourceModel: MongoDataset, - folderTypeList: [DatasetTypeEnum.folder], - session - }); - } else if (dataset.inheritPermission && dataset.parentId) { - const parentClbs = await getResourceAllClbs({ - teamId: dataset.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); - - await syncCollaborators({ - teamId: dataset.teamId, - resourceId: id, - resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbs, - session - }); - } - return onUpdate(session, defaultPermission); - }); } else { return onUpdate(); } diff --git a/projects/app/src/pages/api/support/outLink/update.test.ts b/projects/app/src/pages/api/support/outLink/update.test.ts index 17d3e235ec42..650d33a30a54 100644 --- a/projects/app/src/pages/api/support/outLink/update.test.ts +++ b/projects/app/src/pages/api/support/outLink/update.test.ts @@ -1,12 +1,12 @@ -import { getTestRequest } from '@/test/utils'; import '../../__mocks__/base'; +import { getTestRequest } from '@/test/utils'; import handler, { OutLinkUpdateBody, OutLinkUpdateQuery } from './update'; -import { root } from '../../__mocks__/db/init'; import { MongoOutLink } from '@fastgpt/service/support/outLink/schema'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; +import { root } from '../../__mocks__/db/init'; -test('Update Outlink', async () => { - const outlink = await MongoOutLink.create({ +beforeAll(async () => { + await MongoOutLink.create({ shareId: 'aaa', appId: root.appId, tmbId: root.tmbId, @@ -14,8 +14,13 @@ test('Update Outlink', async () => { type: 'share', name: 'aaa' }); +}); - await outlink.save(); +test('Update Outlink', async () => { + const outlink = await MongoOutLink.findOne({ name: 'aaa' }).lean(); + if (!outlink) { + throw new Error('Outlink not found'); + } const res = (await handler( ...getTestRequest({ @@ -27,6 +32,7 @@ test('Update Outlink', async () => { }) )) as any; + console.log(res); expect(res.code).toBe(200); const link = await MongoOutLink.findById(outlink._id).lean(); diff --git a/projects/app/src/pages/app/detail/components/InfoModal.tsx b/projects/app/src/pages/app/detail/components/InfoModal.tsx index 1e18f56e92b8..d9a3f9b56c44 100644 --- a/projects/app/src/pages/app/detail/components/InfoModal.tsx +++ b/projects/app/src/pages/app/detail/components/InfoModal.tsx @@ -28,16 +28,13 @@ import { } from '@/web/core/app/api/collaborator'; import { useContextSelector } from 'use-context-selector'; import { AppContext } from '@/pages/app/detail/components/context'; -import { - AppDefaultPermissionVal, - AppPermissionList -} from '@fastgpt/global/support/permission/app/constant'; -import DefaultPermissionList from '@/components/support/permission/DefaultPerList'; +import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { resumeInheritPer } from '@/web/core/app/api'; import { useI18n } from '@/web/context/I18n'; import ResumeInherit from '@/components/support/permission/ResumeInheritText'; import { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; const InfoModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); @@ -67,8 +64,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { await updateAppDetail({ name: data.name, avatar: data.avatar, - intro: data.intro, - defaultPermission: data.defaultPermission + intro: data.intro }); }, { @@ -129,24 +125,25 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { const onUpdateCollaborators = ({ members, + groups, permission }: { - members: string[]; + members?: string[]; + groups?: string[]; permission: PermissionValueType; - }) => { - return postUpdateAppCollaborators({ + }) => + postUpdateAppCollaborators({ members, + groups, permission, appId: appDetail._id }); - }; - const onDelCollaborator = (tmbId: string) => { - return deleteAppCollaborators({ + const onDelCollaborator = async (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => + deleteAppCollaborators({ appId: appDetail._id, - tmbId + ...props }); - }; const { runAsync: resumeInheritPermission } = useRequest2(() => resumeInheritPer(appDetail._id), { errorToast: t('common:resume_failed'), @@ -204,33 +201,19 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { )} - - {t('common:permission.Default permission')} - { - setValue('defaultPermission', v); - return handleSubmit((data) => saveSubmitSuccess(data), saveSubmitError)(); - }} - hasParent={!!appDetail.parentId} - /> - getCollaboratorList(appDetail._id)} permissionList={AppPermissionList} - onUpdateCollaborators={(props) => { - if (props.members) { - return onUpdateCollaborators({ - permission: props.permission, - members: props.members - }); - } - }} + onUpdateCollaborators={async (props) => + onUpdateCollaborators({ + permission: props.permission, + members: props.members, + groups: props.groups + }) + } onDelOneCollaborator={onDelCollaborator} refreshDeps={[appDetail.inheritPermission]} isInheritPermission={appDetail.inheritPermission} diff --git a/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx b/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx index 8d854f8ce3fe..f7ffd1f6cb26 100644 --- a/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/SimpleApp/AppCard.tsx @@ -5,7 +5,6 @@ import { Button, IconButton, HStack, - Modal, ModalBody, Checkbox, ModalFooter @@ -19,26 +18,23 @@ import TagsEditModal from '../TagsEditModal'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import { AppContext } from '@/pages/app/detail/components/context'; import { useContextSelector } from 'use-context-selector'; -import PermissionIconText from '@/components/support/permission/IconText'; -import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useI18n } from '@/web/context/I18n'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { postTransition2Workflow } from '@/web/core/app/api/app'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; -import { useSystem } from '@fastgpt/web/hooks/useSystem'; const AppCard = () => { const router = useRouter(); const { t } = useTranslation(); const { appT } = useI18n(); - const { isPc } = useSystem(); const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector( AppContext, (v) => v ); + const appId = appDetail._id; const { feConfigs } = useSystemStore(); const [TeamTagsSet, setTeamTagsSet] = useState(); @@ -150,15 +146,15 @@ const AppCard = () => { /> )} - {isPc && ( - (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} - > - - - )} + {/* {isPc && ( */} + {/* (appDetail.permission.hasManagePer ? onOpenInfoEdit() : undefined)} */} + {/* > */} + {/* */} + {/* */} + {/* )} */} {TeamTagsSet && setTeamTagsSet(undefined)} />} diff --git a/projects/app/src/pages/app/detail/components/context.tsx b/projects/app/src/pages/app/detail/components/context.tsx index 71ca7c9a0ce5..a25632fc3ee1 100644 --- a/projects/app/src/pages/app/detail/components/context.tsx +++ b/projects/app/src/pages/app/detail/components/context.tsx @@ -1,4 +1,4 @@ -import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { Dispatch, ReactNode, SetStateAction, useCallback, useState } from 'react'; import { createContext } from 'use-context-selector'; import { defaultApp } from '@/web/core/app/constants'; import { delAppById, getAppDetailById, putAppById } from '@/web/core/app/api'; diff --git a/projects/app/src/pages/app/list/components/List.tsx b/projects/app/src/pages/app/list/components/List.tsx index f407e124f3fa..2dbef7ae11e1 100644 --- a/projects/app/src/pages/app/list/components/List.tsx +++ b/projects/app/src/pages/app/list/components/List.tsx @@ -17,10 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag'; import dynamic from 'next/dynamic'; import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; -import { - AppDefaultPermissionVal, - AppPermissionList -} from '@fastgpt/global/support/permission/app/constant'; +import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant'; import { deleteAppCollaborators, getCollaboratorList, @@ -38,6 +35,7 @@ import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useChatStore } from '@/web/core/chat/context/storeChat'; import { useUserStore } from '@/web/support/user/useUserStore'; +import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const ListItem = () => { @@ -49,11 +47,16 @@ const ListItem = () => { const { loadAndGetTeamMembers } = useUserStore(); const { lastChatAppId, setLastChatAppId } = useChatStore(); + const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({ + type: 'common', + title: t('common:move.confirm'), + content: t('app:move.hint') + }); + const { myApps, loadMyApps, onUpdateApp, setMoveAppId, folderDetail } = useContextSelector( AppListContext, (v) => v ); - const [loadingAppId, setLoadingAppId] = useState(); const [editedApp, setEditedApp] = useState(); const [editHttpPlugin, setEditHttpPlugin] = useState(); @@ -64,17 +67,20 @@ const ListItem = () => { [editPerAppIndex, myApps] ); + const parentApp = useMemo(() => myApps.find((item) => item._id === parentId), [parentId, myApps]); + + const { runAsync: onPutAppById } = useRequest2(putAppById, { + onSuccess() { + loadMyApps(); + } + }); + const { getBoxProps } = useFolderDrag({ activeStyles: { borderColor: 'primary.600' }, - onDrop: async (dragId: string, targetId: string) => { - setLoadingAppId(dragId); - try { - await putAppById(dragId, { parentId: targetId }); - loadMyApps(); - } catch (error) {} - setLoadingAppId(undefined); + onDrop: (dragId: string, targetId: string) => { + openMoveConfirm(async () => onPutAppById(dragId, { parentId: targetId }))(); } }); @@ -152,7 +158,6 @@ const ListItem = () => { } > { )} { {formatTimeToChatTime(app.updateTime)} )} - {app.permission.hasWritePer && ( + {(AppFolderTypeList.includes(app.type) + ? app.permission.hasManagePer + : app.permission.hasWritePer) && ( { } } }, - ...(folderDetail?.type === AppTypeEnum.httpPlugin + ...(folderDetail?.type === AppTypeEnum.httpPlugin && + !(parentApp ? parentApp.permission : app.permission) + .hasManagePer ? [] : [ { @@ -412,34 +421,29 @@ const ListItem = () => { isInheritPermission={editPerApp.inheritPermission} avatar={editPerApp.avatar} name={editPerApp.name} - defaultPer={{ - value: editPerApp.defaultPermission, - defaultValue: AppDefaultPermissionVal, - onChange: (e) => { - return onUpdateApp(editPerApp._id, { defaultPermission: e }); - } - }} managePer={{ + mode: 'all', permission: editPerApp.permission, onGetCollaboratorList: () => getCollaboratorList(editPerApp._id), permissionList: AppPermissionList, - onUpdateCollaborators: ({ - members = [], // TODO: remove the default value after group is ready - permission - }: { + onUpdateCollaborators: (props: { members?: string[]; + groups?: string[]; permission: number; - }) => { - return postUpdateAppCollaborators({ - members, - permission, + }) => + postUpdateAppCollaborators({ + ...props, appId: editPerApp._id - }); - }, - onDelOneCollaborator: (tmbId: string) => + }), + onDelOneCollaborator: async ( + props: RequireOnlyOne<{ + tmbId?: string; + groupId?: string; + }> + ) => deleteAppCollaborators({ - appId: editPerApp._id, - tmbId + ...props, + appId: editPerApp._id }), refreshDeps: [editPerApp.inheritPermission] }} @@ -452,6 +456,7 @@ const ListItem = () => { onClose={() => setEditHttpPlugin(undefined)} /> )} + ); }; diff --git a/projects/app/src/pages/app/list/components/context.tsx b/projects/app/src/pages/app/list/components/context.tsx index cac2c8140ee2..81b3aa1d4c8f 100644 --- a/projects/app/src/pages/app/list/components/context.tsx +++ b/projects/app/src/pages/app/list/components/context.tsx @@ -12,9 +12,9 @@ import { } from '@fastgpt/global/common/parentFolder/type'; import { AppUpdateParams } from '@/global/core/app/api'; import dynamic from 'next/dynamic'; -import { useI18n } from '@/web/context/I18n'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useTranslation } from 'next-i18next'; const MoveModal = dynamic(() => import('@/components/common/folder/MoveModal')); type AppListContextType = { @@ -58,7 +58,7 @@ export const AppListContext = createContext({ }); const AppListContextProvider = ({ children }: { children: ReactNode }) => { - const { appT } = useI18n(); + const { t } = useTranslation(); const router = useRouter(); const { parentId = null, type = 'ALL' } = router.query as { parentId?: string | null; @@ -129,10 +129,12 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => { parentId, type: AppTypeEnum.folder }).then((res) => - res.map((item) => ({ - id: item._id, - name: item.name - })) + res + .filter((item) => item.permission.hasWritePer) + .map((item) => ({ + id: item._id, + name: item.name + })) ); }, []); @@ -162,9 +164,10 @@ const AppListContextProvider = ({ children }: { children: ReactNode }) => { setMoveAppId(undefined)} onConfirm={onMoveApp} + moveHint={t('app:move.hint')} /> )} diff --git a/projects/app/src/pages/app/list/index.tsx b/projects/app/src/pages/app/list/index.tsx index e324c3644c20..21881c94cf9c 100644 --- a/projects/app/src/pages/app/list/index.tsx +++ b/projects/app/src/pages/app/list/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Box, Flex, @@ -14,8 +14,6 @@ import { useUserStore } from '@/web/support/user/useUserStore'; import { useI18n } from '@/web/context/I18n'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; - -import List from './components/List'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { FolderIcon } from '@fastgpt/global/common/file/image/constants'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; @@ -27,10 +25,7 @@ import FolderPath from '@/components/common/folder/Path'; import { useRouter } from 'next/router'; import FolderSlideCard from '@/components/common/folder/SlideCard'; import { delAppById, resumeInheritPer } from '@/web/core/app/api'; -import { - AppDefaultPermissionVal, - AppPermissionList -} from '@fastgpt/global/support/permission/app/constant'; +import { AppPermissionList } from '@fastgpt/global/support/permission/app/constant'; import { deleteAppCollaborators, getCollaboratorList, @@ -49,6 +44,7 @@ const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') ); const HttpEditModal = dynamic(() => import('./components/HttpPluginEditModal')); +const List = dynamic(() => import('./components/List')); const MyApps = () => { const { t } = useTranslation(); @@ -273,36 +269,47 @@ const MyApps = () => { onMove={() => setMoveAppId(folderDetail._id)} deleteTip={appT('confirm_delete_folder_tip')} onDelete={() => onDeleFolder(folderDetail._id)} - defaultPer={{ - value: folderDetail.defaultPermission, - defaultValue: AppDefaultPermissionVal, - onChange: (e) => { - return onUpdateApp(folderDetail._id, { defaultPermission: e }); - } - }} managePer={{ + mode: 'all', permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), permissionList: AppPermissionList, onUpdateCollaborators: ({ - members = [], // TODO: remove the default value after group is ready + members, + groups, permission }: { members?: string[]; + groups?: string[]; permission: number; }) => { return postUpdateAppCollaborators({ members, + groups, permission, appId: folderDetail._id }); }, refreshDeps: [folderDetail._id, folderDetail.inheritPermission], - onDelOneCollaborator: (tmbId: string) => - deleteAppCollaborators({ - appId: folderDetail._id, - tmbId - }) + onDelOneCollaborator: async ({ + tmbId, + groupId + }: { + tmbId?: string; + groupId?: string; + }) => { + if (tmbId) { + return deleteAppCollaborators({ + appId: folderDetail._id, + tmbId + }); + } else if (groupId) { + return deleteAppCollaborators({ + appId: folderDetail._id, + groupId + }); + } + } }} /> diff --git a/projects/app/src/pages/dataset/detail/components/Info.tsx b/projects/app/src/pages/dataset/detail/components/Info.tsx index b5d38794ce09..4911709a90c6 100644 --- a/projects/app/src/pages/dataset/detail/components/Info.tsx +++ b/projects/app/src/pages/dataset/detail/components/Info.tsx @@ -1,7 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useRouter } from 'next/router'; import { Box, Flex, Input } from '@chakra-ui/react'; -import { delDatasetById } from '@/web/core/dataset/api'; import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useForm } from 'react-hook-form'; @@ -10,7 +8,7 @@ import type { DatasetItemType } from '@fastgpt/global/core/dataset/type.d'; import Avatar from '@fastgpt/web/components/common/Avatar'; import { useTranslation } from 'next-i18next'; import { useSystemStore } from '@/web/common/system/useSystemStore'; -import { useRequest, useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants'; import AIModelSelector from '@/components/Select/AIModelSelector'; import { postRebuildEmbedding } from '@/web/core/dataset/api'; @@ -21,12 +19,8 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider/index'; import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; -import DefaultPermissionList from '@/components/support/permission/DefaultPerList'; import MyIcon from '@fastgpt/web/components/common/Icon'; -import { - DatasetDefaultPermissionVal, - DatasetPermissionList -} from '@fastgpt/global/support/permission/dataset/constant'; +import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant'; import MemberManager from '../../component/MemberManager'; import { getCollaboratorList, @@ -39,7 +33,6 @@ import { EditResourceInfoFormType } from '@/components/common/Modal/EditResource const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const Info = ({ datasetId }: { datasetId: string }) => { - const router = useRouter(); const [openBaseConfig, setOpenBaseConfig] = useState(true); const [openPermissionConfig, setOpenPermissionConfig] = useState(true); const { t } = useTranslation(); @@ -56,10 +49,9 @@ const Info = ({ datasetId }: { datasetId: string }) => { const vectorModel = watch('vectorModel'); const agentModel = watch('agentModel'); - const defaultPermission = watch('defaultPermission'); const { datasetModelList, vectorModelList } = useSystemStore(); - const { openConfirm: onOpenConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({ + const { ConfirmModal: ConfirmDelModal } = useConfirm({ content: t('common:core.dataset.Delete Confirm'), type: 'delete' }); @@ -69,30 +61,17 @@ const Info = ({ datasetId }: { datasetId: string }) => { type: 'delete' }); - const { File, onOpen: onOpenSelectFile } = useSelectFile({ + const { File } = useSelectFile({ fileType: '.jpg,.png', multiple: false }); - /* 点击删除 */ - const { mutate: onclickDelete, isLoading: isDeleting } = useRequest({ - mutationFn: () => { - return delDatasetById(datasetId); - }, - onSuccess() { - router.replace(`/dataset/list`); - }, - successToast: t('common:common.Delete Success'), - errorToast: t('common:common.Delete Failed') - }); - - const { runAsync: onSave, loading: isSaving } = useRequest2( + const { runAsync: onSave } = useRequest2( (data: DatasetItemType) => { return updateDataset({ id: datasetId, agentModel: data.agentModel, - externalReadUrl: data.externalReadUrl, - defaultPermission: data.defaultPermission + externalReadUrl: data.externalReadUrl }); }, { @@ -101,7 +80,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { } ); - const { runAsync: onSelectFile, loading: isSelecting } = useRequest2( + const { runAsync: onSelectFile } = useRequest2( (e: File[]) => { const file = e[0]; if (!file) return Promise.resolve(null); @@ -122,7 +101,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { } ); - const { runAsync: onRebuilding, loading: isRebuilding } = useRequest2( + const { runAsync: onRebuilding } = useRequest2( (vectorModel: VectorModelItemType) => { return postRebuildEmbedding({ datasetId, @@ -242,10 +221,9 @@ const Info = ({ datasetId }: { datasetId: string }) => { onchange={(e) => { const vectorModel = vectorModelList.find((item) => item.model === e); if (!vectorModel) return; - return onOpenConfirmRebuild(() => { - return onRebuilding(vectorModel).then(() => { - setValue('vectorModel', vectorModel); - }); + return onOpenConfirmRebuild(async () => { + await onRebuilding(vectorModel); + setValue('vectorModel', vectorModel); })(); }} /> @@ -326,20 +304,12 @@ const Info = ({ datasetId }: { datasetId: string }) => { {t('common:permission.Default permission')} - { - setValue('defaultPermission', v); - return handleSubmit((data) => onSave({ ...data, defaultPermission: v }))(); - }} - /> getCollaboratorList(datasetId), permissionList: DatasetPermissionList, @@ -348,11 +318,19 @@ const Info = ({ datasetId }: { datasetId: string }) => { ...body, datasetId }), - onDelOneCollaborator: (tmbId) => - deleteDatasetCollaborators({ - datasetId, - tmbId - }) + onDelOneCollaborator: async ({ groupId, tmbId }) => { + if (tmbId) { + return deleteDatasetCollaborators({ + datasetId, + tmbId + }); + } else if (groupId) { + return deleteDatasetCollaborators({ + datasetId, + groupId + }); + } + } }} /> diff --git a/projects/app/src/pages/dataset/list/component/List.tsx b/projects/app/src/pages/dataset/list/component/List.tsx index f15706165261..20a1ae9d50ec 100644 --- a/projects/app/src/pages/dataset/list/component/List.tsx +++ b/projects/app/src/pages/dataset/list/component/List.tsx @@ -18,10 +18,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import dynamic from 'next/dynamic'; import { useContextSelector } from 'use-context-selector'; import { DatasetsContext } from '../context'; -import { - DatasetDefaultPermissionVal, - DatasetPermissionList -} from '@fastgpt/global/support/permission/dataset/constant'; +import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant'; import ConfigPerModal from '@/components/support/permission/ConfigPerModal'; import { deleteDatasetCollaborators, @@ -34,7 +31,6 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import { useI18n } from '@/web/context/I18n'; import { useTranslation } from 'next-i18next'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; import SideTag from './SideTag'; @@ -42,7 +38,6 @@ const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditRe function List() { const { setLoading } = useSystemStore(); - const { toast } = useToast(); const { isPc } = useSystem(); const { t } = useTranslation(); const { commonT } = useI18n(); @@ -59,21 +54,32 @@ function List() { folderDetail } = useContextSelector(DatasetsContext, (v) => v); const [editPerDatasetIndex, setEditPerDatasetIndex] = useState(); - const [loadingDatasetId, setLoadingDatasetId] = useState(); + const router = useRouter(); + const { parentId = null } = router.query as { parentId?: string | null }; + const parentDataset = useMemo( + () => myDatasets.find((item) => String(item._id) === parentId), + [parentId, myDatasets] + ); + + const { openConfirm: openMoveConfirm, ConfirmModal: MoveConfirmModal } = useConfirm({ + type: 'common', + title: t('common:move.confirm'), + content: t('dataset:move.hint') + }); + + const { runAsync: updateDataset } = useRequest2(onUpdateDataset); const { getBoxProps } = useFolderDrag({ activeStyles: { borderColor: 'primary.600' }, - onDrop: async (dragId: string, targetId: string) => { - setLoadingDatasetId(dragId); - try { - await onUpdateDataset({ + onDrop: (dragId: string, targetId: string) => { + openMoveConfirm(() => + updateDataset({ id: dragId, parentId: targetId - }); - } catch (error) {} - setLoadingDatasetId(undefined); + }) + )(); } }); @@ -86,10 +92,6 @@ function List() { [editPerDatasetIndex, myDatasets] ); - const router = useRouter(); - - const { parentId = null } = router.query as { parentId?: string | null }; - const { mutate: exportDataset } = useRequest({ mutationFn: async (dataset: DatasetItemType) => { setLoading(true); @@ -100,15 +102,10 @@ function List() { filename: `${dataset.name}.csv` }); }, - onSuccess() { - toast({ - status: 'success', - title: t('common:core.dataset.Start export') - }); - }, onSettled() { setLoading(false); }, + successToast: t('common:core.dataset.Start export'), errorToast: t('common:dataset.Export Dataset Limit Error') }); @@ -176,7 +173,6 @@ function List() { } > )} @@ -293,7 +289,9 @@ function List() { )} - {dataset.permission.hasWritePer && ( + {(dataset.type === DatasetTypeEnum.folder + ? dataset.permission.hasManagePer + : dataset.permission.hasWritePer) && ( setMoveDatasetId(dataset._id) - }, + ...((parentDataset ? parentDataset : dataset)?.permission + .hasManagePer + ? [ + { + icon: 'common/file/move', + label: t('common:Move'), + onClick: () => { + setMoveDatasetId(dataset._id); + } + } + ] + : []), ...(dataset.permission.hasManagePer ? [ { @@ -427,36 +432,20 @@ function List() { } avatar={editPerDataset.avatar} name={editPerDataset.name} - defaultPer={{ - value: editPerDataset.defaultPermission, - defaultValue: DatasetDefaultPermissionVal, - onChange: (e) => - onUpdateDataset({ - id: editPerDataset._id, - defaultPermission: e - }) - }} managePer={{ + mode: 'all', permission: editPerDataset.permission, onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id), permissionList: DatasetPermissionList, - onUpdateCollaborators: ({ - members = [], // TODO: remove default value after group is ready - permission - }: { - members?: string[]; - permission: number; - }) => { - return postUpdateDatasetCollaborators({ - members, - permission, + onUpdateCollaborators: (props) => + postUpdateDatasetCollaborators({ + ...props, datasetId: editPerDataset._id - }); - }, - onDelOneCollaborator: (tmbId: string) => + }), + onDelOneCollaborator: async (props) => deleteDatasetCollaborators({ - datasetId: editPerDataset._id, - tmbId + ...props, + datasetId: editPerDataset._id }), refreshDeps: [editPerDataset._id, editPerDataset.inheritPermission] }} @@ -464,6 +453,7 @@ function List() { /> )} + ); } diff --git a/projects/app/src/pages/dataset/list/component/MoveModal.tsx b/projects/app/src/pages/dataset/list/component/MoveModal.tsx deleted file mode 100644 index d2f748fa4bf4..000000000000 --- a/projects/app/src/pages/dataset/list/component/MoveModal.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { - Card, - Flex, - Box, - Button, - ModalBody, - ModalHeader, - ModalFooter, - useTheme, - Grid -} from '@chakra-ui/react'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; -import MyModal from '@fastgpt/web/components/common/MyModal'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { useTranslation } from 'next-i18next'; -import { useQuery } from '@tanstack/react-query'; -import { getDatasets, putDatasetById, getDatasetPaths } from '@/web/core/dataset/api'; -import { useRequest } from '@fastgpt/web/hooks/useRequest'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; - -const MoveModal = ({ - onClose, - onSuccess, - moveDataId -}: { - onClose: () => void; - onSuccess: () => void; - moveDataId: string; -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - - const [parentId, setParentId] = useState(''); - - const { data } = useQuery(['getDatasets', parentId], () => { - return Promise.all([ - getDatasets({ parentId, type: DatasetTypeEnum.folder }), - getDatasetPaths(parentId) - ]); - }); - const paths = useMemo( - () => [ - { - parentId: '', - parentName: t('common:core.dataset.My Dataset') - }, - ...(data?.[1] || []) - ], - [data, t] - ); - const folderList = useMemo( - () => (data?.[0] || []).filter((item) => item._id !== moveDataId), - [moveDataId, data] - ); - - const { mutate, isLoading } = useRequest({ - mutationFn: () => putDatasetById({ id: moveDataId, parentId }), - onSuccess, - errorToast: t('common:dataset.Move Failed') - }); - - return ( - - {!!parentId ? ( - - {paths.map((item, i) => ( - - { - setParentId(item.parentId); - } - })} - > - {item.parentName} - - {i !== paths.length - 1 && ( - - )} - - ))} - - ) : ( - {t('common:core.dataset.My Dataset')} - )} - - } - onClose={onClose} - > - - - - {folderList.map((item) => - (() => { - return ( - - { - setParentId(item._id); - }} - > - - - - {item.name} - - - - {item.type === DatasetTypeEnum.folder ? ( - {t('common:Folder')} - ) : ( - <> - - {item.vectorModel.name} - - )} - - - - ); - })() - )} - - {folderList.length === 0 && ( - - )} - - - - - - - - ); -}; - -export default MoveModal; diff --git a/projects/app/src/pages/dataset/list/context.tsx b/projects/app/src/pages/dataset/list/context.tsx index 10edc09262a5..b4608c84cf84 100644 --- a/projects/app/src/pages/dataset/list/context.tsx +++ b/projects/app/src/pages/dataset/list/context.tsx @@ -13,7 +13,6 @@ import { import { useRouter } from 'next/router'; import React, { useCallback, useState } from 'react'; import { createContext } from 'use-context-selector'; -import { useI18n } from '@/web/context/I18n'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api'; import dynamic from 'next/dynamic'; @@ -68,7 +67,6 @@ export const DatasetsContext = createContext({ function DatasetContextProvider({ children }: { children: React.ReactNode }) { const router = useRouter(); - const { commonT } = useI18n(); const { t } = useTranslation(); const [moveDatasetId, setMoveDatasetId] = useState(); const [searchKey, setSearchKey] = useState(''); @@ -127,10 +125,12 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) { parentId, type: DatasetTypeEnum.folder }) - ).map((item) => ({ - id: item._id, - name: item.name - })); + ) + .filter((item) => item.permission.hasManagePer) + .map((item) => ({ + id: item._id, + name: item.name + })); }, []); const [editedDataset, setEditedDataset] = useState(); @@ -164,9 +164,10 @@ function DatasetContextProvider({ children }: { children: React.ReactNode }) { setMoveDatasetId(undefined)} - onConfirm={onMoveDataset} + onConfirm={(parentId) => onMoveDataset(parentId)} + moveHint={t('dataset:move.hint')} /> )} diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index a3db766af5ee..1658dda82a8b 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -17,10 +17,7 @@ import { EditFolderFormType } from '@fastgpt/web/components/common/MyModal/EditF import dynamic from 'next/dynamic'; import { postCreateDatasetFolder, resumeInheritPer } from '@/web/core/dataset/api'; import FolderSlideCard from '@/components/common/folder/SlideCard'; -import { - DatasetDefaultPermissionVal, - DatasetPermissionList -} from '@fastgpt/global/support/permission/dataset/constant'; +import { DatasetPermissionList } from '@fastgpt/global/support/permission/dataset/constant'; import { postUpdateDatasetCollaborators, deleteDatasetCollaborators, @@ -52,7 +49,6 @@ const Dataset = () => { loadMyDatasets, refetchFolderDetail, folderDetail, - setEditedDataset, setMoveDatasetId, onDelDataset, onUpdateDataset, @@ -228,38 +224,39 @@ const Dataset = () => { }); }) } - defaultPer={{ - value: folderDetail.defaultPermission, - defaultValue: DatasetDefaultPermissionVal, - onChange: (e) => { - return onUpdateDataset({ - id: folderDetail._id, - defaultPermission: e - }); - } - }} managePer={{ + mode: 'all', permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), permissionList: DatasetPermissionList, onUpdateCollaborators: ({ - members = [], // TODO: remove the default value after group is ready + members, + groups, permission }: { members?: string[]; + groups?: string[]; permission: number; - }) => { - return postUpdateDatasetCollaborators({ + }) => + postUpdateDatasetCollaborators({ members, + groups, permission, datasetId: folderDetail._id - }); - }, - onDelOneCollaborator: (tmbId: string) => - deleteDatasetCollaborators({ - datasetId: folderDetail._id, - tmbId }), + onDelOneCollaborator: async ({ tmbId, groupId }) => { + if (tmbId) { + return deleteDatasetCollaborators({ + datasetId: folderDetail._id, + tmbId + }); + } else if (groupId) { + return deleteDatasetCollaborators({ + datasetId: folderDetail._id, + groupId + }); + } + }, refreshDeps: [folderDetail._id, folderDetail.inheritPermission] }} /> diff --git a/projects/app/src/test/utils.ts b/projects/app/src/test/utils.ts index cd5723e0bc1d..933cf7487ba3 100644 --- a/projects/app/src/test/utils.ts +++ b/projects/app/src/test/utils.ts @@ -64,7 +64,7 @@ export function getTestRequest({ ]; } -export const MockParseHeaderCert = async ({ +export const parseHeaderCertMock = async ({ req, authToken = true, authRoot = false, diff --git a/projects/app/src/web/core/app/constants.ts b/projects/app/src/web/core/app/constants.ts index ec69273324a6..47b3ab830523 100644 --- a/projects/app/src/web/core/app/constants.ts +++ b/projects/app/src/web/core/app/constants.ts @@ -2,8 +2,6 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppDetailType } from '@fastgpt/global/core/app/type.d'; import type { FeishuAppType, OutLinkEditType } from '@fastgpt/global/support/outLink/type.d'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; -import { NullPermission } from '@fastgpt/global/support/permission/constant'; -import { i18nT } from '@fastgpt/web/i18n/utils'; export const defaultApp: AppDetailType = { _id: '', name: 'AI', @@ -18,7 +16,6 @@ export const defaultApp: AppDetailType = { teamTags: [], edges: [], version: 'v2', - defaultPermission: NullPermission, permission: new AppPermission(), inheritPermission: false }; diff --git a/projects/app/src/web/core/dataset/api/collaborator.ts b/projects/app/src/web/core/dataset/api/collaborator.ts index 05e76876f320..9e75607a4f1e 100644 --- a/projects/app/src/web/core/dataset/api/collaborator.ts +++ b/projects/app/src/web/core/dataset/api/collaborator.ts @@ -11,5 +11,5 @@ export const getCollaboratorList = (datasetId: string) => export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => POST('/proApi/core/dataset/collaborator/update', body); -export const deleteDatasetCollaborators = ({ ...params }: DatasetCollaboratorDeleteParams) => - DELETE('/proApi/core/dataset/collaborator/delete', { ...params }); +export const deleteDatasetCollaborators = (params: DatasetCollaboratorDeleteParams) => + DELETE('/proApi/core/dataset/collaborator/delete', params); diff --git a/projects/app/src/web/core/dataset/constants.ts b/projects/app/src/web/core/dataset/constants.ts index cf94ceba5750..9a815d013c6a 100644 --- a/projects/app/src/web/core/dataset/constants.ts +++ b/projects/app/src/web/core/dataset/constants.ts @@ -8,7 +8,6 @@ import type { DatasetCollectionItemType, DatasetItemType } from '@fastgpt/global/core/dataset/type.d'; -import { DatasetDefaultPermissionVal } from '@fastgpt/global/support/permission/dataset/constant'; import { DatasetPermission } from '@fastgpt/global/support/permission/dataset/controller'; export const defaultDatasetDetail: DatasetItemType = { @@ -26,7 +25,6 @@ export const defaultDatasetDetail: DatasetItemType = { permission: new DatasetPermission(), vectorModel: defaultVectorModels[0], agentModel: defaultQAModels[0], - defaultPermission: DatasetDefaultPermissionVal, inheritPermission: true }; @@ -48,7 +46,6 @@ export const defaultCollectionDetail: DatasetCollectionItemType = { status: 'active', vectorModel: defaultVectorModels[0].model, agentModel: defaultQAModels[0].model, - defaultPermission: DatasetDefaultPermissionVal, inheritPermission: true }, tags: [], diff --git a/projects/app/src/web/support/user/useUserStore.ts b/projects/app/src/web/support/user/useUserStore.ts index 70ae2cba18e0..5cf25a22325c 100644 --- a/projects/app/src/web/support/user/useUserStore.ts +++ b/projects/app/src/web/support/user/useUserStore.ts @@ -28,6 +28,7 @@ type State = { loadAndGetTeamMembers: (init?: boolean) => Promise; teamMemberGroups: MemberGroupListType; + myGroups: MemberGroupListType; loadAndGetGroups: (init?: boolean) => Promise; }; @@ -106,6 +107,7 @@ export const useUserStore = create()( return res; }, teamMemberGroups: [], + myGroups: [], loadAndGetGroups: async (init = false) => { if (!useSystemStore.getState()?.feConfigs?.isPlus) return []; @@ -116,6 +118,9 @@ export const useUserStore = create()( const res = await getGroupList(); set((state) => { state.teamMemberGroups = res; + state.myGroups = res.filter((item) => + item.members.map((i) => String(i.tmbId)).includes(String(state.userInfo?.team?.tmbId)) + ); }); return res;