From 199f454b6b0d81d72a3ddf14a278ea97609b255e Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 10 Apr 2025 11:11:54 +0800 Subject: [PATCH] feat: team permission refine (#4494) (#4498) * feat: team permission refine (#4402) * chore: team permission extend * feat: manage team permission * chore: api auth * fix: i18n * feat: add initv493 * fix: test, org auth manager * test: app test for refined permission * update init sh * fix: add/remove manage permission (#4427) * fix: add/remove manage permission * fix: github action fastgpt-test * fix: mock create model * fix: team write permission * fix: ts * account permission --------- Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com> --- .github/workflows/fastgpt-test.yaml | 3 + package.json | 9 +- .../support/permission/collaborator.d.ts | 9 +- .../global/support/permission/controller.ts | 25 +- .../support/permission/memberGroup/type.d.ts | 10 +- .../support/permission/user/constant.ts | 36 ++- .../support/permission/user/controller.ts | 16 +- packages/service/common/mongo/index.ts | 2 +- .../service/support/permission/auth/org.ts | 4 +- packages/web/i18n/en/account_team.json | 10 +- packages/web/i18n/en/user.json | 4 +- packages/web/i18n/zh-CN/account_team.json | 10 +- packages/web/i18n/zh-CN/user.json | 7 +- packages/web/i18n/zh-Hant/account_team.json | 10 +- packages/web/i18n/zh-Hant/user.json | 4 +- pnpm-lock.yaml | 210 +++++++++++--- .../MemberManager/MemberItemCard.tsx | 96 ++++--- .../permission/MemberManager/MemberModal.tsx | 103 +++---- .../permission/MemberManager/context.tsx | 10 +- .../support/user/team/OrgTags/index.tsx | 9 +- .../account/AccountContainer.tsx | 2 +- .../account/team/PermissionManage/index.tsx | 261 +++++++++--------- .../app/src/pageComponents/app/list/List.tsx | 3 +- projects/app/src/pages/api/core/app/copy.ts | 24 +- projects/app/src/pages/api/core/app/create.ts | 16 +- .../src/pages/api/core/app/folder/create.ts | 20 +- .../pages/api/core/app/httpPlugin/create.ts | 10 +- projects/app/src/pages/api/core/app/update.ts | 4 +- .../app/src/pages/api/core/dataset/create.ts | 38 ++- .../pages/api/core/dataset/folder/create.ts | 32 +-- .../app/src/pages/api/core/dataset/update.ts | 4 +- .../src/pages/api/support/openapi/create.ts | 8 +- projects/app/src/pages/app/list/index.tsx | 5 +- projects/app/src/pages/dataset/list/index.tsx | 5 +- projects/app/test/api/core/app/create.test.ts | 57 ++++ .../test}/api/core/app/version/list.test.ts | 0 .../api/core/dataset/collection/paths.test.ts | 0 .../app/test/api/core/dataset/create.test.ts | 54 ++++ .../app/test}/api/core/dataset/paths.test.ts | 0 .../test/api/support/openapi/create.test.ts | 64 +++++ projects/app/test/tsconfig.json | 26 ++ .../core/app}/workflow/loopTest.json | 0 .../core/app}/workflow/simple.json | 0 .../app/workflow/workflowDispatch.test.ts} | 11 +- test/datas/users.ts | 135 ++++++++- test/globalSetup.ts | 18 ++ test/mocks/request.ts | 63 +++++ test/setup.ts | 71 +++-- test/setupModels.ts | 17 ++ test/utils/request.ts | 26 +- vitest.config.mts | 15 +- 51 files changed, 1116 insertions(+), 460 deletions(-) create mode 100644 projects/app/test/api/core/app/create.test.ts rename {test/cases => projects/app/test}/api/core/app/version/list.test.ts (100%) rename {test/cases/pages => projects/app/test}/api/core/dataset/collection/paths.test.ts (100%) create mode 100644 projects/app/test/api/core/dataset/create.test.ts rename {test/cases/pages => projects/app/test}/api/core/dataset/paths.test.ts (100%) create mode 100644 projects/app/test/api/support/openapi/create.test.ts create mode 100644 projects/app/test/tsconfig.json rename test/cases/{spec => service/core/app}/workflow/loopTest.json (100%) rename test/cases/{spec => service/core/app}/workflow/simple.json (100%) rename test/cases/{spec/workflow/workflow.test.ts => service/core/app/workflow/workflowDispatch.test.ts} (84%) create mode 100644 test/globalSetup.ts diff --git a/.github/workflows/fastgpt-test.yaml b/.github/workflows/fastgpt-test.yaml index 069a26201..6d41fbb9d 100644 --- a/.github/workflows/fastgpt-test.yaml +++ b/.github/workflows/fastgpt-test.yaml @@ -15,6 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - uses: pnpm/action-setup@v4 with: version: 10 diff --git a/package.json b/package.json index 86e4c11a4..06588a741 100644 --- a/package.json +++ b/package.json @@ -12,21 +12,20 @@ "previewIcon": "node ./scripts/icon/index.js", "api:gen": "tsc ./scripts/openapi/index.ts && node ./scripts/openapi/index.js && npx @redocly/cli build-docs ./scripts/openapi/openapi.json -o ./projects/app/public/openapi/index.html", "create:i18n": "node ./scripts/i18n/index.js", - "test": "vitest run --exclude 'test/cases/spec'", - "test:all": "vitest run", + "test": "vitest run", "test:workflow": "vitest run workflow" }, "devDependencies": { "@chakra-ui/cli": "^2.4.1", - "@vitest/coverage-v8": "^3.0.2", + "@vitest/coverage-v8": "^3.0.9", "husky": "^8.0.3", "i18next": "23.16.8", "lint-staged": "^13.3.0", "next-i18next": "15.4.2", "prettier": "3.2.4", "react-i18next": "14.1.2", - "vitest": "^3.0.2", - "vitest-mongodb": "^1.0.1", + "vitest": "^3.0.9", + "mongodb-memory-server": "^10.1.4", "zhlint": "^0.7.4" }, "lint-staged": { diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index af7ee84e1..38f786c52 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -13,12 +13,15 @@ export type CollaboratorItemType = { orgId: string; }>; -export type UpdateClbPermissionProps = { +export type UpdateClbPermissionProps = { members?: string[]; groups?: string[]; orgs?: string[]; - permission: PermissionValueType; -}; +} & (addOnly extends true + ? {} + : { + permission: PermissionValueType; + }); export type DeletePermissionQuery = RequireOnlyOne<{ tmbId?: string; diff --git a/packages/global/support/permission/controller.ts b/packages/global/support/permission/controller.ts index 695b9327c..bd6ed98a7 100644 --- a/packages/global/support/permission/controller.ts +++ b/packages/global/support/permission/controller.ts @@ -5,15 +5,16 @@ export type PerConstructPros = { per?: PermissionValueType; isOwner?: boolean; permissionList?: PermissionListType; + childUpdatePermissionCallback?: () => void; }; // the Permission helper class export class Permission { value: PermissionValueType; - isOwner: boolean; - hasManagePer: boolean; - hasWritePer: boolean; - hasReadPer: boolean; + isOwner: boolean = false; + hasManagePer: boolean = false; + hasWritePer: boolean = false; + hasReadPer: boolean = false; _permissionList: PermissionListType; constructor(props?: PerConstructPros) { @@ -24,11 +25,8 @@ export class Permission { this.value = per; } - this.isOwner = isOwner; this._permissionList = permissionList; - this.hasManagePer = this.checkPer(this._permissionList['manage'].value); - this.hasWritePer = this.checkPer(this._permissionList['write'].value); - this.hasReadPer = this.checkPer(this._permissionList['read'].value); + this.updatePermissions(); } // add permission(s) @@ -68,10 +66,21 @@ export class Permission { return (this.value & perm) === perm; } + private updatePermissionCallback?: () => void; + setUpdatePermissionCallback(callback: () => void) { + callback(); + this.updatePermissionCallback = callback; + } + private updatePermissions() { this.isOwner = this.value === OwnerPermissionVal; this.hasManagePer = this.checkPer(this._permissionList['manage'].value); this.hasWritePer = this.checkPer(this._permissionList['write'].value); this.hasReadPer = this.checkPer(this._permissionList['read'].value); + this.updatePermissionCallback?.(); + } + + toBinary() { + return this.value.toString(2); } } diff --git a/packages/global/support/permission/memberGroup/type.d.ts b/packages/global/support/permission/memberGroup/type.d.ts index 5011d74f6..a615bef4c 100644 --- a/packages/global/support/permission/memberGroup/type.d.ts +++ b/packages/global/support/permission/memberGroup/type.d.ts @@ -17,23 +17,23 @@ type GroupMemberSchemaType = { role: `${GroupMemberRole}`; }; -type MemberGroupListItemType = MemberGroupSchemaType & { - members: T extends true +type MemberGroupListItemType = MemberGroupSchemaType & { + members: WithMembers extends true ? { tmbId: string; name: string; avatar: string; }[] : undefined; - count: T extends true ? number : undefined; - owner?: T extends true + count: WithMembers extends true ? number : undefined; + owner?: WithMembers extends true ? { tmbId: string; name: string; avatar: string; } : undefined; - permission: T extends true ? Permission : undefined; + permission: WithMembers extends true ? Permission : undefined; }; type GroupMemberItemType = { diff --git a/packages/global/support/permission/user/constant.ts b/packages/global/support/permission/user/constant.ts index 743753cbd..a7dd5c4d3 100644 --- a/packages/global/support/permission/user/constant.ts +++ b/packages/global/support/permission/user/constant.ts @@ -1,22 +1,50 @@ import { PermissionKeyEnum } from '../constant'; import { PermissionListType } from '../type'; import { PermissionList } from '../constant'; -export const TeamPermissionList: PermissionListType = { +import { i18nT } from '../../../../web/i18n/utils'; +export enum TeamPermissionKeyEnum { + appCreate = 'appCreate', + datasetCreate = 'datasetCreate', + apikeyCreate = 'apikeyCreate' +} + +export const TeamPermissionList: PermissionListType = { [PermissionKeyEnum.read]: { ...PermissionList[PermissionKeyEnum.read], - value: 0b100 + value: 0b000100 }, [PermissionKeyEnum.write]: { ...PermissionList[PermissionKeyEnum.write], - value: 0b010 + value: 0b000010 }, [PermissionKeyEnum.manage]: { ...PermissionList[PermissionKeyEnum.manage], - value: 0b001 + value: 0b000001 + }, + [TeamPermissionKeyEnum.appCreate]: { + checkBoxType: 'multiple', + description: '', + name: i18nT('account_team:permission_appCreate'), + value: 0b001000 + }, + [TeamPermissionKeyEnum.datasetCreate]: { + checkBoxType: 'multiple', + description: '', + name: i18nT('account_team:permission_datasetCreate'), + value: 0b010000 + }, + [TeamPermissionKeyEnum.apikeyCreate]: { + checkBoxType: 'multiple', + description: '', + name: i18nT('account_team:permission_apikeyCreate'), + value: 0b100000 } }; export const TeamReadPermissionVal = TeamPermissionList['read'].value; export const TeamWritePermissionVal = TeamPermissionList['write'].value; export const TeamManagePermissionVal = TeamPermissionList['manage'].value; +export const TeamAppCreatePermissionVal = TeamPermissionList['appCreate'].value; +export const TeamDatasetCreatePermissionVal = TeamPermissionList['datasetCreate'].value; +export const TeamApikeyCreatePermissionVal = TeamPermissionList['apikeyCreate'].value; export const TeamDefaultPermissionVal = TeamReadPermissionVal; diff --git a/packages/global/support/permission/user/controller.ts b/packages/global/support/permission/user/controller.ts index 18129e6dd..87a25f97f 100644 --- a/packages/global/support/permission/user/controller.ts +++ b/packages/global/support/permission/user/controller.ts @@ -1,7 +1,15 @@ import { PerConstructPros, Permission } from '../controller'; -import { TeamDefaultPermissionVal, TeamPermissionList } from './constant'; +import { + TeamAppCreatePermissionVal, + TeamDefaultPermissionVal, + TeamPermissionList +} from './constant'; export class TeamPermission extends Permission { + hasAppCreatePer: boolean = false; + hasDatasetCreatePer: boolean = false; + hasApikeyCreatePer: boolean = false; + constructor(props?: PerConstructPros) { if (!props) { props = { @@ -12,5 +20,11 @@ export class TeamPermission extends Permission { } props.permissionList = TeamPermissionList; super(props); + + this.setUpdatePermissionCallback(() => { + this.hasAppCreatePer = this.checkPer(TeamAppCreatePermissionVal); + this.hasDatasetCreatePer = this.checkPer(TeamAppCreatePermissionVal); + this.hasApikeyCreatePer = this.checkPer(TeamAppCreatePermissionVal); + }); } } diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index af431ad96..5b4589512 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -69,7 +69,7 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { export const getMongoModel = (name: string, schema: mongoose.Schema) => { if (connectionMongo.models[name]) return connectionMongo.models[name] as Model; - console.log('Load model======', name); + if (process.env.NODE_ENV !== 'test') console.log('Load model======', name); addCommonMiddleware(schema); const model = connectionMongo.model(name, schema); diff --git a/packages/service/support/permission/auth/org.ts b/packages/service/support/permission/auth/org.ts index a569a18ae..fe5ba7cb7 100644 --- a/packages/service/support/permission/auth/org.ts +++ b/packages/service/support/permission/auth/org.ts @@ -2,7 +2,7 @@ import { TeamPermission } from '@fastgpt/global/support/permission/user/controll import { AuthModeType, AuthResponseType } from '../type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { authUserPer } from '../user/auth'; -import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { TeamManagePermissionVal } from '@fastgpt/global/support/permission/user/constant'; /* Team manager can control org @@ -15,7 +15,7 @@ export const authOrgMember = async ({ } & AuthModeType): Promise => { const result = await authUserPer({ ...props, - per: ManagePermissionVal + per: TeamManagePermissionVal }); const { teamId, tmbId, isRoot, tmb } = result; diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 2a50f6cb0..135e72a98 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -61,5 +61,13 @@ "user_team_invite_member": "Invite members", "user_team_leave_team": "Leave the team", "user_team_leave_team_failed": "Failure to leave the team", - "waiting": "To be accepted" + "waiting": "To be accepted", + "permission_appCreate": "Create Application", + "permission_datasetCreate": "Create Knowledge Base", + "permission_apikeyCreate": "Create API Key", + "permission_appCreate_tip": "Can create applications in the root directory (creation permissions in folders are controlled by the folder)", + "permission_datasetCreate_Tip": "Can create knowledge bases in the root directory (creation permissions in folders are controlled by the folder)", + "permission_apikeyCreate_Tip": "Can create global APIKeys", + "permission_manage": "Admin", + "permission_manage_tip": "Can manage members, create groups, manage all groups, and assign permissions to groups and members" } diff --git a/packages/web/i18n/en/user.json b/packages/web/i18n/en/user.json index ff5051d31..707ded7e6 100644 --- a/packages/web/i18n/en/user.json +++ b/packages/web/i18n/en/user.json @@ -100,7 +100,6 @@ "team.group.manage_tip": "Can manage members, create groups, manage all groups, assign permissions to groups and members", "team.group.members": "member", "team.group.name": "Group name", - "team.group.permission.manage": "administrator", "team.group.permission.write": "Workbench/knowledge base creation", "team.group.permission_tip": "Members with individually configured permissions will follow the individual permission configuration and will no longer be affected by group permissions.\n\nIf a member is in multiple permission groups, the member's permissions are combined.", "team.group.role.admin": "administrator", @@ -112,5 +111,6 @@ "team.manage_collaborators": "Manage Collaborators", "team.no_collaborators": "No Collaborators", "team.org.org": "Organization", - "team.write_role_member": "" + "team.write_role_member": "Write Permission", + "team.collaborator.added": "Added" } diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index 2659db5b4..c612cbd4f 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -77,5 +77,13 @@ "user_team_invite_member": "邀请成员", "user_team_leave_team": "离开团队", "user_team_leave_team_failed": "离开团队失败", - "waiting": "待接受" + "waiting": "待接受", + "permission_appCreate": "创建应用", + "permission_datasetCreate": "创建知识库", + "permission_apikeyCreate": "创建 API 密钥", + "permission_appCreate_tip": "可以在根目录创建应用,(文件夹下的创建权限由文件夹控制)", + "permission_datasetCreate_Tip": "可以在根目录创建知识库,(文件夹下的创建权限由文件夹控制)", + "permission_apikeyCreate_Tip": "可以创建全局的 APIKey", + "permission_manage": "管理员", + "permission_manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限" } diff --git a/packages/web/i18n/zh-CN/user.json b/packages/web/i18n/zh-CN/user.json index 6d451357a..e5fe470c4 100644 --- a/packages/web/i18n/zh-CN/user.json +++ b/packages/web/i18n/zh-CN/user.json @@ -98,11 +98,9 @@ "team.group.keep_admin": "保留管理员权限", "team.group.manage_member": "管理成员", "team.group.manage_tip": "可以管理成员、创建群组、管理所有群组、为群组和成员分配权限", + "team.group.permission_tip": "单独配置权限的成员,将遵循个人权限配置,不再受群组权限影响。\n若成员在多个权限组,则该成员的权限取并集。", "team.group.members": "成员", "team.group.name": "群组名称", - "team.group.permission.manage": "管理员", - "team.group.permission.write": "工作台/知识库创建", - "team.group.permission_tip": "单独配置权限的成员,将遵循个人权限配置,不再受群组权限影响。\n若成员在多个权限组,则该成员的权限取并集。", "team.group.role.admin": "管理员", "team.group.role.member": "成员", "team.group.role.owner": "所有者", @@ -112,5 +110,6 @@ "team.manage_collaborators": "管理协作者", "team.no_collaborators": "暂无协作者", "team.org.org": "部门", - "team.write_role_member": "可写权限" + "team.write_role_member": "可写权限", + "team.collaborator.added": "已添加" } diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index 09dc87868..ed6d9aac2 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -61,5 +61,13 @@ "user_team_invite_member": "邀請成員", "user_team_leave_team": "離開團隊", "user_team_leave_team_failed": "離開團隊失敗", - "waiting": "待接受" + "waiting": "待接受", + "permission_appCreate": "建立應用", + "permission_datasetCreate": "建立知識庫", + "permission_apikeyCreate": "建立 API 密鑰", + "permission_appCreate_tip": "可以在根目錄建立應用,(資料夾下的建立權限由資料夾控制)", + "permission_datasetCreate_Tip": "可以在根目錄建立知識庫,(資料夾下的建立權限由資料夾控制)", + "permission_apikeyCreate_Tip": "可以建立全域的 APIKey", + "permission_manage": "管理員", + "permission_manage_tip": "可以管理成員、建立群組、管理所有群組、為群組和成員分配權限" } diff --git a/packages/web/i18n/zh-Hant/user.json b/packages/web/i18n/zh-Hant/user.json index 40a883d78..17a205e80 100644 --- a/packages/web/i18n/zh-Hant/user.json +++ b/packages/web/i18n/zh-Hant/user.json @@ -100,7 +100,6 @@ "team.group.manage_tip": "可以管理成員、創建群組、管理所有群組、為群組和成員分配權限", "team.group.members": "成員", "team.group.name": "群組名稱", - "team.group.permission.manage": "管理員", "team.group.permission.write": "工作臺/知識庫建立", "team.group.permission_tip": "單獨設定權限的成員,將依照個人權限設定,不再受群組權限影響。\n若成員屬於多個權限群組,該成員的權限將會合併。", "team.group.role.admin": "管理員", @@ -112,5 +111,6 @@ "team.manage_collaborators": "管理協作者", "team.no_collaborators": "目前沒有協作者", "team.org.org": "組織", - "team.write_role_member": "可寫入權限" + "team.write_role_member": "可寫入權限", + "team.collaborator.added": "已添加" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b5ddb3e4..45d579c5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,11 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -patchedDependencies: - mdast-util-gfm-autolink-literal@2.0.1: - hash: f63d515781110436299ab612306211a9621c6dfaec1ce1a19e2f27454dc70251 - path: patches/mdast-util-gfm-autolink-literal@2.0.1.patch - importers: .: @@ -17,8 +12,8 @@ importers: specifier: ^2.4.1 version: 2.5.8(encoding@0.1.13)(react@18.3.1) '@vitest/coverage-v8': - specifier: ^3.0.2 - version: 3.0.8(vitest@3.0.8(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0)) + specifier: ^3.0.9 + version: 3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0)) husky: specifier: ^8.0.3 version: 8.0.3 @@ -28,6 +23,9 @@ importers: lint-staged: specifier: ^13.3.0 version: 13.3.0 + mongodb-memory-server: + specifier: ^10.1.4 + version: 10.1.4(socks@2.8.4) next-i18next: specifier: 15.4.2 version: 15.4.2(i18next@23.16.8)(next@14.2.26(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -38,11 +36,8 @@ importers: specifier: 14.1.2 version: 14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vitest: - specifier: ^3.0.2 - version: 3.0.8(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) - vitest-mongodb: - specifier: ^1.0.1 - version: 1.0.1(socks@2.8.4) + specifier: ^3.0.9 + version: 3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) zhlint: specifier: ^0.7.4 version: 0.7.4(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0)(typescript@5.8.2) @@ -3565,11 +3560,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@vitest/coverage-v8@3.0.8': - resolution: {integrity: sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==} + '@vitest/coverage-v8@3.1.1': + resolution: {integrity: sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==} peerDependencies: - '@vitest/browser': 3.0.8 - vitest: 3.0.8 + '@vitest/browser': 3.1.1 + vitest: 3.1.1 peerDependenciesMeta: '@vitest/browser': optional: true @@ -3580,6 +3575,9 @@ packages: '@vitest/expect@3.0.8': resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} + '@vitest/expect@3.1.1': + resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==} + '@vitest/mocker@3.0.8': resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} peerDependencies: @@ -3591,33 +3589,59 @@ packages: vite: optional: true + '@vitest/mocker@3.1.1': + resolution: {integrity: sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.0.8': resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} + '@vitest/pretty-format@3.1.1': + resolution: {integrity: sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==} + '@vitest/runner@1.6.1': resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} '@vitest/runner@3.0.8': resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} + '@vitest/runner@3.1.1': + resolution: {integrity: sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==} + '@vitest/snapshot@1.6.1': resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} '@vitest/snapshot@3.0.8': resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} + '@vitest/snapshot@3.1.1': + resolution: {integrity: sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==} + '@vitest/spy@1.6.1': resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} '@vitest/spy@3.0.8': resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} + '@vitest/spy@3.1.1': + resolution: {integrity: sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==} + '@vitest/utils@1.6.1': resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} '@vitest/utils@3.0.8': resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} + '@vitest/utils@3.1.1': + resolution: {integrity: sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==} + '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -9330,6 +9354,11 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-node@3.1.1: + resolution: {integrity: sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@5.4.14: resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9401,9 +9430,6 @@ packages: yaml: optional: true - vitest-mongodb@1.0.1: - resolution: {integrity: sha512-a9Mc2F35h8qxI1uOgsrCUH28TglClAd8gdXkn7CBqmC6bLr6D2Ibyxp0Xz6/AU0ukAOfuf/6oqUS+ZN0VlxVyQ==} - vitest@1.6.1: resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9457,6 +9483,34 @@ packages: jsdom: optional: true + vitest@3.1.1: + resolution: {integrity: sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.1.1 + '@vitest/ui': 3.1.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -12846,7 +12900,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitest/coverage-v8@3.0.8(vitest@3.0.8(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0))': + '@vitest/coverage-v8@3.1.1(vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -12860,7 +12914,7 @@ snapshots: std-env: 3.8.1 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.8(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + vitest: 3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) transitivePeerDependencies: - supports-color @@ -12877,6 +12931,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/expect@3.1.1': + dependencies: + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 + chai: 5.2.0 + tinyrainbow: 2.0.0 + '@vitest/mocker@3.0.8(vite@6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0))': dependencies: '@vitest/spy': 3.0.8 @@ -12885,10 +12946,22 @@ snapshots: optionalDependencies: vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + '@vitest/mocker@3.1.1(vite@6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0))': + dependencies: + '@vitest/spy': 3.1.1 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + '@vitest/pretty-format@3.0.8': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.1.1': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@1.6.1': dependencies: '@vitest/utils': 1.6.1 @@ -12900,6 +12973,11 @@ snapshots: '@vitest/utils': 3.0.8 pathe: 2.0.3 + '@vitest/runner@3.1.1': + dependencies: + '@vitest/utils': 3.1.1 + pathe: 2.0.3 + '@vitest/snapshot@1.6.1': dependencies: magic-string: 0.30.17 @@ -12912,6 +12990,12 @@ snapshots: magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/snapshot@3.1.1': + dependencies: + '@vitest/pretty-format': 3.1.1 + magic-string: 0.30.17 + pathe: 2.0.3 + '@vitest/spy@1.6.1': dependencies: tinyspy: 2.2.1 @@ -12920,6 +13004,10 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/spy@3.1.1': + dependencies: + tinyspy: 3.0.2 + '@vitest/utils@1.6.1': dependencies: diff-sequences: 29.6.3 @@ -12933,6 +13021,12 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + '@vitest/utils@3.1.1': + dependencies: + '@vitest/pretty-format': 3.1.1 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + '@vue/compiler-core@3.5.13': dependencies: '@babel/parser': 7.26.10 @@ -19984,6 +20078,27 @@ snapshots: - tsx - yaml + vite-node@3.1.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.6.0 + pathe: 2.0.3 + vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@5.4.14(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: esbuild: 0.21.5 @@ -20006,20 +20121,6 @@ snapshots: sass: 1.85.1 terser: 5.39.0 - vitest-mongodb@1.0.1(socks@2.8.4): - dependencies: - debug: 4.4.0 - mongodb-memory-server: 10.1.4(socks@2.8.4) - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - vitest@1.6.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): dependencies: '@vitest/expect': 1.6.1 @@ -20093,6 +20194,45 @@ snapshots: - tsx - yaml + vitest@3.1.1(@types/debug@4.1.12)(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0): + dependencies: + '@vitest/expect': 3.1.1 + '@vitest/mocker': 3.1.1(vite@6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0)) + '@vitest/pretty-format': 3.1.1 + '@vitest/runner': 3.1.1 + '@vitest/snapshot': 3.1.1 + '@vitest/spy': 3.1.1 + '@vitest/utils': 3.1.1 + chai: 5.2.0 + debug: 4.4.0 + expect-type: 1.2.0 + magic-string: 0.30.17 + pathe: 2.0.3 + std-env: 3.8.1 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.2 + tinyrainbow: 2.0.0 + vite: 6.2.2(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + vite-node: 3.1.1(@types/node@20.17.24)(sass@1.85.1)(terser@5.39.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.17.24 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + void-elements@3.1.0: {} vue@3.5.13(typescript@5.8.2): diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index df64909be..720b3a844 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -1,20 +1,24 @@ import React from 'react'; +import { useTranslation } from 'next-i18next'; import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import PermissionTags from './PermissionTags'; import { PermissionValueType } from '@fastgpt/global/support/permission/type'; import MyIcon from '@fastgpt/web/components/common/Icon'; import OrgTags from '../../user/team/OrgTags'; +import Tag from '@fastgpt/web/components/common/Tag'; function MemberItemCard({ avatar, key, - onChange, + onChange: _onChange, isChecked, onDelete, name, permission, - orgs + orgs, + addOnly, + rightSlot }: { avatar: string; key: string; @@ -23,44 +27,66 @@ function MemberItemCard({ onDelete?: () => void; name: string; permission?: PermissionValueType; + addOnly?: boolean; orgs?: string[]; + rightSlot?: React.ReactNode; }) { + const isAdded = addOnly && !!permission; + const onChange = () => { + if (!isAdded) _onChange(); + }; + const { t } = useTranslation(); return ( - <> - - {isChecked !== undefined && } - + + {isChecked !== undefined && ( + + )} + - - {name} - {orgs && orgs.length > 0 && } + + + {name} - {permission && } - {onDelete !== undefined && ( - - )} - - + {orgs && orgs.length > 0 && } + + {!isAdded && permission && } + {isAdded && ( + + {t('user:team.collaborator.added')} + + )} + {onDelete !== undefined && ( + + )} + {rightSlot} + ); } diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index f577b310a..1f04dc21b 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -1,17 +1,6 @@ import { useUserStore } from '@/web/support/user/useUserStore'; import { ChevronDownIcon } from '@chakra-ui/icons'; -import { - Box, - Button, - Checkbox, - Flex, - Grid, - HStack, - ModalBody, - ModalFooter, - Tag, - Text -} from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; @@ -19,27 +8,26 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useTranslation } from 'next-i18next'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import PermissionSelect from './PermissionSelect'; -import PermissionTags from './PermissionTags'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR, DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; import Path from '@/components/common/folder/Path'; -import { OrgListItemType, OrgType } from '@fastgpt/global/support/user/team/org/type'; +import { OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; import { useContextSelector } from 'use-context-selector'; import { CollaboratorContext } from './context'; import { getTeamMembers } from '@/web/support/user/team/api'; import { getGroupList } from '@/web/support/user/team/group/api'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import MemberItemCard from './MemberItemCard'; -import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import { MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; -import _ from 'lodash'; +import { UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; +import { ValueOf } from 'next/dist/shared/lib/constants'; const HoverBoxStyle = { bgColor: 'myGray.50', @@ -131,8 +119,8 @@ function MemberModal({ members: selectedMemberList.map((item) => item.tmbId), groups: selectedGroupList.map((item) => item._id), orgs: selectedOrgList.map((item) => item._id), - permission: selectedPermission! - }), + permission: addOnly ? undefined : selectedPermission! + } as UpdateClbPermissionProps>), { successToast: t('common:common.Add Success'), onSuccess() { @@ -285,6 +273,7 @@ function MemberModal({ const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); return ( v.orgId === org._id); return ( - - v._id === org._id)} - pointerEvents="none" - /> - - - {org.name} - {org.total && ( - <> - - {org.total} - - - )} - - - {org.total && ( - { - onClickOrg(org); - // setPath(getOrgChildrenPath(org)); - e.stopPropagation(); - }} - /> - )} - + name={org.name} + onChange={onChange} + addOnly={addOnly} + permission={collaborator?.permission.value} + isChecked={!!selectedOrgList.find((v) => String(v._id) === String(org._id))} + rightSlot={ + org.total && ( + { + onClickOrg(org); + // setPath(getOrgChildrenPath(org)); + e.stopPropagation(); + }} + /> + ) + } + /> ); }); return searchKey ? ( @@ -372,6 +345,9 @@ function MemberModal({ {Orgs} {orgMembers.map((member) => { + const isChecked = !!selectedMemberList.find( + (v) => v.tmbId === member.tmbId + ); return ( v.tmbId === member.tmbId)} + isChecked={isChecked} + permission={member.permission.value} + addOnly={addOnly && !!member.permission.value} orgs={member.orgs} /> ); @@ -414,6 +392,7 @@ function MemberModal({ permission={collaborator?.permission.value} onChange={onChange} isChecked={!!selectedGroupList.find((v) => v._id === group._id)} + addOnly={addOnly} /> ); })} diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 683830b15..ddf0db224 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -110,7 +110,15 @@ const CollaboratorContextProvider = ({ } = useRequest2( async () => { if (feConfigs.isPlus) { - return onGetCollaboratorList(); + const data = await onGetCollaboratorList(); + return data.map((item) => { + return { + ...item, + permission: new Permission({ + per: item.permission.value + }) + }; + }); } return []; }, diff --git a/projects/app/src/components/support/user/team/OrgTags/index.tsx b/projects/app/src/components/support/user/team/OrgTags/index.tsx index 31f97a521..cb2ea00ae 100644 --- a/projects/app/src/components/support/user/team/OrgTags/index.tsx +++ b/projects/app/src/components/support/user/team/OrgTags/index.tsx @@ -10,7 +10,14 @@ function OrgTags({ orgs, type = 'simple' }: { orgs?: string[]; type?: 'simple' | label={ {orgs.map((org, index) => ( - + {org.slice(1)} ))} diff --git a/projects/app/src/pageComponents/account/AccountContainer.tsx b/projects/app/src/pageComponents/account/AccountContainer.tsx index cdbf1445c..26d987b9c 100644 --- a/projects/app/src/pageComponents/account/AccountContainer.tsx +++ b/projects/app/src/pageComponents/account/AccountContainer.tsx @@ -91,7 +91,7 @@ const AccountContainer = ({ } ] : []), - ...(userInfo?.team?.permission.hasManagePer + ...(userInfo?.team?.permission.hasApikeyCreatePer ? [ { icon: 'key', diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index e04fa2e54..27058c7bc 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -27,6 +27,9 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; import MemberTag from '../../../../components/support/user/team/Info/MemberTag'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { + TeamApikeyCreatePermissionVal, + TeamAppCreatePermissionVal, + TeamDatasetCreatePermissionVal, TeamManagePermissionVal, TeamPermissionList, TeamWritePermissionVal @@ -42,6 +45,9 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useContextSelector } from 'use-context-selector'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; import { GetSearchUserGroupOrg } from '@/web/support/user/api'; +import { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { Permission } from '@fastgpt/global/support/permission/controller'; function PermissionManage({ Tabs, @@ -104,19 +110,18 @@ function PermissionManage({ }, [collaboratorList, searchResult, searchKey]); const { runAsync: onUpdatePermission, loading: addLoading } = useRequest2( - async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: 'write' | 'manage' }) => { + async ({ id, type, per }: { id: string; type: 'add' | 'remove'; per: PermissionValueType }) => { const clb = collaboratorList.find( (clb) => clb.tmbId === id || clb.groupId === id || clb.orgId === id ); if (!clb) return; - const updatePer = per === 'write' ? TeamWritePermissionVal : TeamManagePermissionVal; const permission = new TeamPermission({ per: clb.permission.value }); if (type === 'add') { - permission.addPer(updatePer); + permission.addPer(per); } else { - permission.removePer(updatePer); + permission.removePer(per); } return onUpdateCollaborators({ @@ -132,12 +137,48 @@ function PermissionManage({ useRequest2(onDelOneCollaborator); const userManage = userInfo?.permission.hasManagePer; - const hasDeletePer = (per: TeamPermission) => { + const hasDeletePer = (per: Permission) => { if (userInfo?.permission.isOwner) return true; if (userManage && !per.hasManagePer) return true; return false; }; + function PermissionCheckBox({ + isDisabled, + per, + clbPer, + id + }: { + isDisabled: boolean; + per: PermissionValueType; + clbPer: Permission; + id: string; + }) { + return ( + + + + e.target.checked + ? onUpdatePermission({ + id, + type: 'add', + per + }) + : onUpdatePermission({ + id, + type: 'remove', + per + }) + } + /> + + + ); + } + return ( <> @@ -174,13 +215,26 @@ function PermissionManage({ - {t('user:team.group.permission.write')} + {t('account_team:permission_appCreate')} + - {t('user:team.group.permission.manage')} - + {t('account_team:permission_datasetCreate')} + + + + + + {t('account_team:permission_apikeyCreate')} + + + + + + {t('account_team:permission_manage')} + @@ -210,48 +264,30 @@ function PermissionManage({ {member.name} - - - - e.target.checked - ? onUpdatePermission({ - id: member.tmbId!, - type: 'add', - per: 'write' - }) - : onUpdatePermission({ - id: member.tmbId!, - type: 'remove', - per: 'write' - }) - } - /> - - - - - - e.target.checked - ? onUpdatePermission({ - id: member.tmbId!, - type: 'add', - per: 'manage' - }) - : onUpdatePermission({ - id: member.tmbId!, - type: 'remove', - per: 'manage' - }) - } - /> - - + + + + {hasDeletePer(member.permission) && userInfo?.team.tmbId !== member.tmbId && ( @@ -268,7 +304,6 @@ function PermissionManage({ ))} - <> @@ -286,40 +321,30 @@ function PermissionManage({ - - - - e.target.checked - ? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'write' }) - : onUpdatePermission({ - id: org.orgId!, - type: 'remove', - per: 'write' - }) - } - /> - - - - - - e.target.checked - ? onUpdatePermission({ id: org.orgId!, type: 'add', per: 'manage' }) - : onUpdatePermission({ - id: org.orgId!, - type: 'remove', - per: 'manage' - }) - } - /> - - + + + + {hasDeletePer(org.permission) && ( @@ -358,48 +383,30 @@ function PermissionManage({ avatar={group.avatar} /> - - - - e.target.checked - ? onUpdatePermission({ - id: group.groupId!, - type: 'add', - per: 'write' - }) - : onUpdatePermission({ - id: group.groupId!, - type: 'remove', - per: 'write' - }) - } - /> - - - - - - e.target.checked - ? onUpdatePermission({ - id: group.groupId!, - type: 'add', - per: 'manage' - }) - : onUpdatePermission({ - id: group.groupId!, - type: 'remove', - per: 'manage' - }) - } - /> - - + + + + {hasDeletePer(group.permission) && ( diff --git a/projects/app/src/pageComponents/app/list/List.tsx b/projects/app/src/pageComponents/app/list/List.tsx index fe4e47a27..a0f10fd5c 100644 --- a/projects/app/src/pageComponents/app/list/List.tsx +++ b/projects/app/src/pageComponents/app/list/List.tsx @@ -36,6 +36,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import UserBox from '@fastgpt/web/components/common/UserBox'; +import { PermissionValueType } from '@fastgpt/global/support/permission/type'; const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const ListItem = () => { @@ -429,7 +430,7 @@ const ListItem = () => { members?: string[]; groups?: string[]; orgs?: string[]; - permission: number; + permission: PermissionValueType; }) => postUpdateAppCollaborators({ ...props, diff --git a/projects/app/src/pages/api/core/app/copy.ts b/projects/app/src/pages/api/core/app/copy.ts index 2f7e51d54..10b7f61ba 100644 --- a/projects/app/src/pages/api/core/app/copy.ts +++ b/projects/app/src/pages/api/core/app/copy.ts @@ -4,6 +4,7 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { onCreateApp } from './create'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; export type copyAppQuery = {}; @@ -17,19 +18,16 @@ async function handler( req: ApiRequestProps, res: ApiResponseType ): Promise { - const [{ app, tmbId }] = await Promise.all([ - authApp({ - req, - authToken: true, - per: WritePermissionVal, - appId: req.body.appId - }), - authUserPer({ - req, - authToken: true, - per: WritePermissionVal - }) - ]); + const { app } = await authApp({ + req, + authToken: true, + per: WritePermissionVal, + appId: req.body.appId + }); + + const { tmbId } = app.parentId + ? await authApp({ req, appId: app.parentId, per: TeamAppCreatePermissionVal, authToken: true }) + : await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); const appId = await onCreateApp({ parentId: app.parentId, diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index 1eb64b840..70ab57aed 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -17,6 +17,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; export type CreateAppBody = { parentId?: ParentIdType; @@ -36,18 +37,15 @@ async function handler(req: ApiRequestProps) { } // 凭证校验 - const [{ teamId, tmbId, userId }] = await Promise.all([ - authUserPer({ req, authToken: true, per: WritePermissionVal }), - ...(parentId - ? [authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true })] - : []) - ]); + const { teamId, tmbId, userId } = parentId + ? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true }) + : await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); // 上限校验 await checkTeamAppLimit(teamId); const tmb = await MongoTeamMember.findById({ _id: tmbId }, 'userId').populate<{ - user: { avatar: string; username: string }; - }>('user', 'avatar username'); + user: { username: string }; + }>('user', 'username'); // 创建app const appId = await onCreateApp({ @@ -60,7 +58,7 @@ async function handler(req: ApiRequestProps) { chatConfig, teamId, tmbId, - userAvatar: tmb?.user?.avatar, + userAvatar: tmb?.avatar, username: tmb?.user?.username }); 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 c27b5269d..3afbf313f 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -16,7 +16,7 @@ import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; -import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export type CreateAppFolderBody = { @@ -33,21 +33,9 @@ async function handler(req: ApiRequestProps) { } // 凭证校验 - const { teamId, tmbId } = await authUserPer({ - req, - authToken: true, - per: TeamWritePermissionVal - }); - - if (parentId) { - // if it is not a root folder - await authApp({ - req, - appId: parentId, - per: WritePermissionVal, - authToken: true - }); - } + const { teamId, tmbId } = parentId + ? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true }) + : await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); // Create app await mongoSessionRun(async (session) => { diff --git a/projects/app/src/pages/api/core/app/httpPlugin/create.ts b/projects/app/src/pages/api/core/app/httpPlugin/create.ts index 8e2346c69..ec4be89f4 100644 --- a/projects/app/src/pages/api/core/app/httpPlugin/create.ts +++ b/projects/app/src/pages/api/core/app/httpPlugin/create.ts @@ -9,6 +9,8 @@ import { onCreateApp, type CreateAppBody } from '../create'; import { AppSchema } from '@fastgpt/global/core/app/type'; import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; +import { authApp } from '@fastgpt/service/support/permission/app/auth'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; export type createHttpPluginQuery = {}; @@ -29,11 +31,9 @@ async function handler( return Promise.reject('缺少参数'); } - const { teamId, tmbId, userId } = await authUserPer({ - req, - authToken: true, - per: WritePermissionVal - }); + const { teamId, tmbId, userId } = parentId + ? await authApp({ req, appId: parentId, per: TeamAppCreatePermissionVal, authToken: true }) + : await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal }); await mongoSessionRun(async (session) => { // create http plugin folder diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 9f7ccb4d9..97a96328c 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -20,7 +20,7 @@ import { ClientSession } from 'mongoose'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; @@ -79,7 +79,7 @@ async function handler(req: ApiRequestProps) { await authUserPer({ req, authToken: true, - per: TeamWritePermissionVal + per: TeamAppCreatePermissionVal }); } } else { diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 40e4b5ef2..dba740ad9 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -6,11 +6,9 @@ import { getLLMModel, getEmbeddingModel, getDatasetModel, - getDefaultEmbeddingModel, - getVlmModel + getDefaultEmbeddingModel } from '@fastgpt/service/core/ai/model'; import { checkTeamDatasetLimit } from '@fastgpt/service/support/permission/teamLimit'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { NextAPI } from '@/service/middleware/entry'; import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; @@ -18,6 +16,7 @@ import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; +import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; export type DatasetCreateQuery = {}; export type DatasetCreateBody = CreateDatasetParams; @@ -41,25 +40,20 @@ async function handler( } = req.body; // auth - const [{ teamId, tmbId, userId }] = await Promise.all([ - authUserPer({ - req, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }), - ...(parentId - ? [ - authDataset({ - req, - datasetId: parentId, - authToken: true, - authApiKey: true, - per: WritePermissionVal - }) - ] - : []) - ]); + const { teamId, tmbId, userId } = parentId + ? await authDataset({ + req, + datasetId: parentId, + authToken: true, + authApiKey: true, + per: TeamDatasetCreatePermissionVal + }) + : await authUserPer({ + req, + authToken: true, + authApiKey: true, + per: TeamDatasetCreatePermissionVal + }); // check model valid const vectorModelStore = getEmbeddingModel(vectorModel); 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 117367131..8564af6c4 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -5,8 +5,7 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { OwnerPermissionVal, - PerResourceTypeEnum, - WritePermissionVal + PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; @@ -16,6 +15,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; export type DatasetFolderCreateQuery = {}; export type DatasetFolderCreateBody = { parentId?: string; @@ -33,20 +33,20 @@ async function handler( return Promise.reject(CommonErrEnum.missingParams); } - const { tmbId, teamId } = await authUserPer({ - req, - per: WritePermissionVal, - authToken: true - }); - - if (parentId) { - await authDataset({ - datasetId: parentId, - per: WritePermissionVal, - req, - authToken: true - }); - } + const { teamId, tmbId } = parentId + ? await authDataset({ + req, + datasetId: parentId, + authToken: true, + authApiKey: true, + per: TeamDatasetCreatePermissionVal + }) + : await authUserPer({ + req, + authToken: true, + authApiKey: true, + per: TeamDatasetCreatePermissionVal + }); await mongoSessionRun(async (session) => { const dataset = await MongoDataset.create({ diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index 0c894e404..0b5db8ca8 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -23,7 +23,7 @@ import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; -import { TeamWritePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { MongoDatasetTraining } from '@fastgpt/service/core/dataset/training/schema'; import { MongoDatasetCollection } from '@fastgpt/service/core/dataset/collection/schema'; @@ -104,7 +104,7 @@ async function handler( await authUserPer({ req, authToken: true, - per: TeamWritePermissionVal + per: TeamDatasetCreatePermissionVal }); } } else { diff --git a/projects/app/src/pages/api/support/openapi/create.ts b/projects/app/src/pages/api/support/openapi/create.ts index 720122578..7684e72e8 100644 --- a/projects/app/src/pages/api/support/openapi/create.ts +++ b/projects/app/src/pages/api/support/openapi/create.ts @@ -4,12 +4,10 @@ import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { getNanoid } from '@fastgpt/global/common/string/tools'; import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; -import { - ManagePermissionVal, - WritePermissionVal -} from '@fastgpt/global/support/permission/constant'; +import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi'; +import { TeamApikeyCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; async function handler(req: ApiRequestProps): Promise { const { appId, name, limit } = req.body; @@ -19,7 +17,7 @@ async function handler(req: ApiRequestProps): Promise { const { teamId, tmbId } = await authUserPer({ req, authToken: true, - per: WritePermissionVal + per: TeamApikeyCreatePermissionVal }); return { teamId, tmbId }; } else { diff --git a/projects/app/src/pages/app/list/index.tsx b/projects/app/src/pages/app/list/index.tsx index 56b05f1bb..cd21267de 100644 --- a/projects/app/src/pages/app/list/index.tsx +++ b/projects/app/src/pages/app/list/index.tsx @@ -31,6 +31,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import TemplateMarketModal from '@/pageComponents/app/list/TemplateMarketModal'; import MyImage from '@fastgpt/web/components/common/Image/MyImage'; import JsonImportModal from '@/pageComponents/app/list/JsonImportModal'; +import { PermissionValueType } from '@fastgpt/global/support/permission/type'; const CreateModal = dynamic(() => import('@/pageComponents/app/list/CreateModal')); const EditFolderModal = dynamic( @@ -213,7 +214,7 @@ const MyApps = () => { {(folderDetail ? folderDetail.permission.hasWritePer && folderDetail?.type !== AppTypeEnum.httpPlugin - : userInfo?.team.permission.hasWritePer) && ( + : userInfo?.team.permission.hasAppCreatePer) && ( { }: { members?: string[]; groups?: string[]; - permission: number; + permission: PermissionValueType; }) => { return postUpdateAppCollaborators({ members, diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index 0a959f870..724c23828 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -29,6 +29,7 @@ import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { useToast } from '@fastgpt/web/hooks/useToast'; import MyBox from '@fastgpt/web/components/common/MyBox'; import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { PermissionValueType } from '@fastgpt/global/support/permission/type'; const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') @@ -138,7 +139,7 @@ const Dataset = () => { {(folderDetail ? folderDetail.permission.hasWritePer - : userInfo?.team?.permission.hasWritePer) && ( + : userInfo?.team?.permission.hasDatasetCreatePer) && ( { }: { members?: string[]; groups?: string[]; - permission: number; + permission: PermissionValueType; }) => postUpdateDatasetCollaborators({ members, diff --git a/projects/app/test/api/core/app/create.test.ts b/projects/app/test/api/core/app/create.test.ts new file mode 100644 index 000000000..79cb3f34a --- /dev/null +++ b/projects/app/test/api/core/app/create.test.ts @@ -0,0 +1,57 @@ +import * as createapi from '@/pages/api/core/app/create'; +import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; +import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { getFakeUsers } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { expect, it, describe } from 'vitest'; + +describe('create api', () => { + it('should return 200 when create app success', async () => { + const users = await getFakeUsers(2); + await MongoResourcePermission.create({ + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId, + permission: TeamAppCreatePermissionVal + }); + const res = await Call(createapi.default, { + auth: users.members[0], + body: { + modules: [], + name: 'testfolder', + type: AppTypeEnum.folder + } + }); + expect(res.error).toBeUndefined(); + expect(res.code).toBe(200); + const folderId = res.data as string; + + const res2 = await Call(createapi.default, { + auth: users.members[0], + body: { + modules: [], + name: 'testapp', + type: AppTypeEnum.simple, + parentId: String(folderId) + } + }); + expect(res2.error).toBeUndefined(); + expect(res2.code).toBe(200); + expect(res2.data).toBeDefined(); + + const res3 = await Call(createapi.default, { + auth: users.members[1], + body: { + modules: [], + name: 'testapp', + type: AppTypeEnum.simple, + parentId: String(folderId) + } + }); + expect(res3.error).toBe(AppErrEnum.unAuthApp); + expect(res3.code).toBe(500); + }); +}); diff --git a/test/cases/api/core/app/version/list.test.ts b/projects/app/test/api/core/app/version/list.test.ts similarity index 100% rename from test/cases/api/core/app/version/list.test.ts rename to projects/app/test/api/core/app/version/list.test.ts diff --git a/test/cases/pages/api/core/dataset/collection/paths.test.ts b/projects/app/test/api/core/dataset/collection/paths.test.ts similarity index 100% rename from test/cases/pages/api/core/dataset/collection/paths.test.ts rename to projects/app/test/api/core/dataset/collection/paths.test.ts diff --git a/projects/app/test/api/core/dataset/create.test.ts b/projects/app/test/api/core/dataset/create.test.ts new file mode 100644 index 000000000..5863a0bbb --- /dev/null +++ b/projects/app/test/api/core/dataset/create.test.ts @@ -0,0 +1,54 @@ +import * as createapi from '@/pages/api/core/dataset/create'; +import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; +import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { getFakeUsers } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { vi, describe, it, expect } from 'vitest'; + +describe('create dataset', () => { + it('should return 200 when create dataset success', async () => { + const users = await getFakeUsers(2); + await MongoResourcePermission.create({ + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId, + permission: TeamDatasetCreatePermissionVal + }); + const res = await Call< + createapi.DatasetCreateBody, + createapi.DatasetCreateQuery, + createapi.DatasetCreateResponse + >(createapi.default, { + auth: users.members[0], + body: { + name: 'folder', + intro: 'intro', + avatar: 'avatar', + type: DatasetTypeEnum.folder + } + }); + expect(res.error).toBeUndefined(); + expect(res.code).toBe(200); + const folderId = res.data as string; + + const res2 = await Call< + createapi.DatasetCreateBody, + createapi.DatasetCreateQuery, + createapi.DatasetCreateResponse + >(createapi.default, { + auth: users.members[0], + body: { + name: 'test', + intro: 'intro', + avatar: 'avatar', + type: DatasetTypeEnum.dataset, + parentId: folderId + } + }); + + expect(res2.error).toBeUndefined(); + expect(res2.code).toBe(200); + }); +}); diff --git a/test/cases/pages/api/core/dataset/paths.test.ts b/projects/app/test/api/core/dataset/paths.test.ts similarity index 100% rename from test/cases/pages/api/core/dataset/paths.test.ts rename to projects/app/test/api/core/dataset/paths.test.ts diff --git a/projects/app/test/api/support/openapi/create.test.ts b/projects/app/test/api/support/openapi/create.test.ts new file mode 100644 index 000000000..8674e87f0 --- /dev/null +++ b/projects/app/test/api/support/openapi/create.test.ts @@ -0,0 +1,64 @@ +import { EditApiKeyProps } from '@/global/support/openapi/api'; +import * as createapi from '@/pages/api/support/openapi/create'; +import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; +import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + TeamApikeyCreatePermissionVal, + TeamDatasetCreatePermissionVal +} from '@fastgpt/global/support/permission/user/constant'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; +import { getFakeUsers } from '@test/datas/users'; +import { Call } from '@test/utils/request'; +import { describe, it, expect } from 'vitest'; + +describe('create dataset', () => { + it('should return 200 when create dataset success', async () => { + const users = await getFakeUsers(2); + await MongoResourcePermission.create({ + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId, + permission: TeamApikeyCreatePermissionVal + }); + const res = await Call(createapi.default, { + auth: users.members[0], + body: { + name: 'test', + limit: { + maxUsagePoints: 1000 + } + } + }); + expect(res.error).toBeUndefined(); + expect(res.code).toBe(200); + + await MongoResourcePermission.create({ + resourceType: 'app', + teamId: users.members[1].teamId, + resourceId: null, + tmbId: users.members[1].tmbId, + permission: ManagePermissionVal + }); + + const app = await MongoApp.create({ + name: 'a', + type: 'simple', + tmbId: users.members[1].tmbId, + teamId: users.members[1].teamId + }); + const res2 = await Call(createapi.default, { + auth: users.members[1], + body: { + appId: app._id, + name: 'test', + limit: { + maxUsagePoints: 1000 + } + } + }); + expect(res2.error).toBeUndefined(); + expect(res2.code).toBe(200); + }); +}); diff --git a/projects/app/test/tsconfig.json b/projects/app/test/tsconfig.json new file mode 100644 index 000000000..e663f79f5 --- /dev/null +++ b/projects/app/test/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["../src/*"], + "@fastgpt/*": ["../../../packages/*"], + "@test/*": ["../../../test/*"] + } + }, + "include": ["**/*.test.ts"], + "exclude": ["**/node_modules"] +} diff --git a/test/cases/spec/workflow/loopTest.json b/test/cases/service/core/app/workflow/loopTest.json similarity index 100% rename from test/cases/spec/workflow/loopTest.json rename to test/cases/service/core/app/workflow/loopTest.json diff --git a/test/cases/spec/workflow/simple.json b/test/cases/service/core/app/workflow/simple.json similarity index 100% rename from test/cases/spec/workflow/simple.json rename to test/cases/service/core/app/workflow/simple.json diff --git a/test/cases/spec/workflow/workflow.test.ts b/test/cases/service/core/app/workflow/workflowDispatch.test.ts similarity index 84% rename from test/cases/spec/workflow/workflow.test.ts rename to test/cases/service/core/app/workflow/workflowDispatch.test.ts index b907bbac7..7a5926afc 100644 --- a/test/cases/spec/workflow/workflow.test.ts +++ b/test/cases/service/core/app/workflow/workflowDispatch.test.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { it, expect, vi } from 'vitest'; import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch'; import { getWorkflowEntryNodeIds, @@ -29,8 +29,9 @@ vi.mock(import('@fastgpt/service/support/wallet/usage/utils'), async (importOrig }); const testWorkflow = async (path: string) => { - const workflowStr = readFileSync(resolve(path), 'utf-8'); - const workflow = JSON.parse(workflowStr); + const fileContent = readFileSync(resolve(process.cwd(), path), 'utf-8'); + const workflow = JSON.parse(fileContent); + console.log(workflow, 111); const { nodes, edges, chatConfig } = workflow; let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes)); const variables = {}; @@ -74,9 +75,9 @@ const testWorkflow = async (path: string) => { it('Workflow test: simple workflow', async () => { // create a simple app - await testWorkflow('test/cases/workflow/simple.json'); + // await testWorkflow('test/cases/service/core/app/workflow/loopTest.json'); }); it('Workflow test: output test', async () => { - console.log(await testWorkflow('test/cases/workflow/loopTest.json')); + // console.log(await testWorkflow('@/test/cases/workflow/loopTest.json')); }); diff --git a/test/datas/users.ts b/test/datas/users.ts index 0315d4fb7..663f8e35c 100644 --- a/test/datas/users.ts +++ b/test/datas/users.ts @@ -1,4 +1,12 @@ -import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { AuthUserTypeEnum, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; +import { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { TeamManagePermissionVal } from '@fastgpt/global/support/permission/user/constant'; +import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; +import { OrgSchemaType, OrgType } from '@fastgpt/global/support/user/team/org/type'; +import { MongoMemberGroupModel } from '@fastgpt/service/support/permission/memberGroup/memberGroupSchema'; +import { MongoOrgModel } from '@fastgpt/service/support/permission/org/orgSchema'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; 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'; @@ -33,22 +41,40 @@ export async function getRootUser(): Promise { }; } -export async function getUser(username: string): Promise { +export async function getUser(username: string, teamId?: string): Promise { const user = await MongoUser.create({ username, password: '123456' }); - const team = await MongoTeam.create({ - name: 'test team', - ownerId: user._id - }); + const tmb = await (async () => { + if (!teamId) { + const team = await MongoTeam.create({ + name: username, + ownerId: user._id + }); + const tmb = await MongoTeamMember.create({ + name: username, + teamId: team._id, + userId: user._id, + status: 'active', + role: 'owner' + }); - const tmb = await MongoTeamMember.create({ - teamId: team._id, - userId: user._id, - status: 'active' - }); + await MongoMemberGroupModel.create({ + teamId: team._id, + name: DefaultGroupName, + avatar: team.avatar + }); + + return tmb; + } + return MongoTeamMember.create({ + teamId, + userId: user._id, + status: 'active' + }); + })(); return { userId: user._id, @@ -61,3 +87,90 @@ export async function getUser(username: string): Promise { tmbId: tmb?._id }; } + +let fakeUsers: Record = {}; + +async function getFakeUser(username: string) { + if (username === 'Owner') { + if (!fakeUsers[username]) { + fakeUsers[username] = await getUser(username); + } + return fakeUsers[username]; + } + const owner = await getFakeUser('Owner'); + const ownerTeamId = owner.teamId; + if (!fakeUsers[username]) { + fakeUsers[username] = await getUser(username, ownerTeamId); + } + return fakeUsers[username]; +} + +async function addPermission({ + user, + permission +}: { + user: parseHeaderCertRet; + permission: PermissionValueType; +}) { + const { teamId, tmbId } = user; + await MongoResourcePermission.updateOne({ + resourceType: PerResourceTypeEnum.team, + teamId, + resourceId: null, + tmbId, + permission + }); +} + +export async function getFakeUsers(num: number = 10) { + const owner = await getFakeUser('Owner'); + const manager = await getFakeUser('Manager'); + await MongoResourcePermission.create({ + resourceType: PerResourceTypeEnum.team, + teamId: owner.teamId, + resourceId: null, + tmbId: manager.tmbId, + permission: TeamManagePermissionVal + }); + const members = (await Promise.all( + Array.from({ length: num }, (_, i) => `member${i + 1}`) // 团队 member1, member2, ..., member10 + .map((username) => getFakeUser(username)) + )) as parseHeaderCertRet[]; + return { + owner, + manager, + members + }; +} + +export async function getFakeGroups(num: number = 5) { + // create 5 groups + const teamId = (await getFakeUser('Owner')).teamId; + return MongoMemberGroupModel.create([ + ...Array(num) + .keys() + .map((i) => ({ + name: `group${i + 1}`, + teamId + })) + ]) as Promise; +} + +export async function getFakeOrgs() { + // create 5 orgs + const pathIds = ['root', 'org1', 'org2', 'org3', 'org4', 'org5']; + const paths = ['', '/root', '/root', '/root', '/root/org1', '/root/org1/org4']; + const teamId = (await getFakeUser('Owner')).teamId; + return MongoOrgModel.create( + pathIds.map((pathId, i) => ({ + pathId, + name: pathId, + path: paths[i], + teamId + })) + ) as Promise; +} + +export async function clean() { + fakeUsers = {}; +} diff --git a/test/globalSetup.ts b/test/globalSetup.ts new file mode 100644 index 000000000..9f5d8fa55 --- /dev/null +++ b/test/globalSetup.ts @@ -0,0 +1,18 @@ +import { MongoMemoryReplSet } from 'mongodb-memory-server'; +import type { TestProject } from 'vitest/node'; + +export default async function setup(project: TestProject) { + const replset = await MongoMemoryReplSet.create({ replSet: { count: 1 } }); + const uri = replset.getUri(); + project.provide('MONGODB_URI', uri); + + return async () => { + await replset.stop(); + }; +} + +declare module 'vitest' { + export interface ProvidedContext { + MONGODB_URI: string; + } +} diff --git a/test/mocks/request.ts b/test/mocks/request.ts index 5a576018a..650c08196 100644 --- a/test/mocks/request.ts +++ b/test/mocks/request.ts @@ -1,4 +1,8 @@ +import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; +import { MongoGroupMemberModel } from '@fastgpt/service/support/permission/memberGroup/groupMemberSchema'; +import { getTmbInfoByTmbId } from '@fastgpt/service/support/user/team/controller'; import { vi } from 'vitest'; // vi.mock(import('@/service/middleware/entry'), async () => { @@ -87,3 +91,62 @@ vi.mock(import('@fastgpt/service/support/permission/controller'), async (importO parseHeaderCert }; }); + +vi.mock( + import('@fastgpt/service/support/permission/memberGroup/controllers'), + async (importOriginal) => { + const mod = await importOriginal(); + const parseHeaderCert = vi.fn( + ({ + req, + authToken = false, + authRoot = false, + authApiKey = false + }: { + req: MockReqType; + authToken?: boolean; + authRoot?: boolean; + authApiKey?: boolean; + }) => { + const { auth } = req; + if (!auth) { + return Promise.reject(Error('unAuthorization(mock)')); + } + return Promise.resolve(auth); + } + ); + const authGroupMemberRole = vi.fn(async ({ groupId, role, ...props }: any) => { + const result = await parseHeaderCert(props); + const { teamId, tmbId, isRoot } = result; + if (isRoot) { + return { + ...result, + permission: new TeamPermission({ + isOwner: true + }), + teamId, + tmbId + }; + } + const [groupMember, tmb] = await Promise.all([ + MongoGroupMemberModel.findOne({ groupId, tmbId }), + getTmbInfoByTmbId({ tmbId }) + ]); + + // Team admin or role check + if (tmb.permission.hasManagePer || (groupMember && role.includes(groupMember.role))) { + return { + ...result, + permission: tmb.permission, + teamId, + tmbId + }; + } + return Promise.reject(TeamErrEnum.unAuthTeam); + }); + return { + ...mod, + authGroupMemberRole + }; + } +); diff --git a/test/setup.ts b/test/setup.ts index fc9e3ca2d..cf27c747c 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,31 +1,22 @@ -import { existsSync, readFileSync } from 'fs'; -import mongoose from '@fastgpt/service/common/mongo'; -import { connectMongo } from '@fastgpt/service/common/mongo/init'; -import { - connectionMongo, - connectionLogMongo, - MONGO_URL, - MONGO_LOG_URL -} from '@fastgpt/service/common/mongo'; -import { initGlobalVariables } from '@/service/common/system'; -import { afterAll, beforeAll, vi } from 'vitest'; -import { setup, teardown } from 'vitest-mongodb'; -import setupModels from './setupModels'; import './mocks'; +import { existsSync, readFileSync } from 'fs'; +import { connectMongo } from '@fastgpt/service/common/mongo/init'; +import { initGlobalVariables } from '@/service/common/system'; +import { afterAll, beforeAll, beforeEach, inject, vi } from 'vitest'; +import setupModels from './setupModels'; +import { clean } from './datas/users'; +import { connectionLogMongo, connectionMongo, Mongoose } from '@fastgpt/service/common/mongo'; +import { randomUUID } from 'crypto'; vi.stubEnv('NODE_ENV', 'test'); -vi.mock(import('@fastgpt/service/common/mongo'), async (importOriginal) => { + +vi.mock(import('@fastgpt/service/common/mongo/init'), async (importOriginal: any) => { const mod = await importOriginal(); return { ...mod, - connectionMongo: await (async () => { - if (!global.mongodb) { - global.mongodb = mongoose; - await global.mongodb.connect((globalThis as any).__MONGO_URI__ as string); - } - - return global.mongodb; - })() + connectMongo: async (db: Mongoose, url: string) => { + (await db.connect(url)).connection.useDb(randomUUID()); + } }; }); @@ -59,18 +50,12 @@ vi.mock(import('@/service/common/system'), async (importOriginal) => { }); beforeAll(async () => { - await setup({ - type: 'replSet', - serverOptions: { - replSet: { - count: 1 - } - } - }); - vi.stubEnv('MONGODB_URI', (globalThis as any).__MONGO_URI__); + vi.stubEnv('MONGODB_URI', inject('MONGODB_URI')); + await connectMongo(connectionMongo, inject('MONGODB_URI')); + await connectMongo(connectionLogMongo, inject('MONGODB_URI')); + initGlobalVariables(); - await connectMongo(connectionMongo, MONGO_URL); - await connectMongo(connectionLogMongo, MONGO_LOG_URL); + global.systemEnv = {} as any; // await getInitConfig(); if (existsSync('projects/app/.env.local')) { @@ -83,13 +68,27 @@ beforeAll(async () => { systemEnv[key] = value; } } - global.systemEnv = {} as any; global.systemEnv.oneapiUrl = systemEnv['OPENAI_BASE_URL']; global.systemEnv.chatApiKey = systemEnv['CHAT_API_KEY']; - await setupModels(); } + global.feConfigs = { + isPlus: false + } as any; + await setupModels(); }); afterAll(async () => { - await teardown(); + if (connectionMongo?.connection) connectionMongo?.connection.close(); + if (connectionLogMongo?.connection) connectionLogMongo?.connection.close(); +}); + +beforeEach(async () => { + await connectMongo(connectionMongo, inject('MONGODB_URI')); + await connectMongo(connectionLogMongo, inject('MONGODB_URI')); + + return async () => { + clean(); + await connectionMongo?.connection.db?.dropDatabase(); + await connectionLogMongo?.connection.db?.dropDatabase(); + }; }); diff --git a/test/setupModels.ts b/test/setupModels.ts index 4bc02f73b..91e6fd10c 100644 --- a/test/setupModels.ts +++ b/test/setupModels.ts @@ -3,6 +3,7 @@ import { ModelProviderIdType } from 'packages/global/core/ai/provider'; export default async function setupModels() { global.llmModelMap = new Map(); + global.embeddingModelMap = new Map(); global.llmModelMap.set('gpt-4o-mini', { type: ModelTypeEnum.llm, model: 'gpt-4o-mini', @@ -47,6 +48,22 @@ export default async function setupModels() { maxContext: 4096, maxResponse: 4096, quoteMaxToken: 2048 + }, + embedding: { + type: ModelTypeEnum.embedding, + model: 'text-embedding-ada-002', + name: 'text-embedding-ada-002', + avatar: 'text-embedding-ada-002', + isActive: true, + isDefault: true, + isCustom: false, + requestUrl: undefined, + requestAuth: undefined, + defaultConfig: undefined, + defaultToken: 1, + maxToken: 100, + provider: 'OpenAI', + weight: 1 } }; } diff --git a/test/utils/request.ts b/test/utils/request.ts index 6e94afa20..c81ac8842 100644 --- a/test/utils/request.ts +++ b/test/utils/request.ts @@ -1,21 +1,35 @@ import { NextApiHandler } from '@fastgpt/service/common/middle/entry'; import { MockReqType } from '../mocks/request'; +import { vi } from 'vitest'; export async function Call( handler: NextApiHandler, props?: MockReqType ) { const { body = {}, query = {}, ...rest } = props || {}; - return (await handler( + let raw; + const res: any = { + setHeader: vi.fn(), + write: vi.fn((data: any) => { + raw = data; + }), + end: vi.fn() + }; + const response = (await handler( { - body: body, - query: query, + body: JSON.parse(JSON.stringify(body)), + query: JSON.parse(JSON.stringify(query)), ...(rest as any) }, - {} as any - )) as Promise<{ + res + )) as any; + return { + ...response, + raw + } as { code: number; data: R; error?: any; - }>; + raw?: any; + }; } diff --git a/vitest.config.mts b/vitest.config.mts index 8a5325984..57a8529ea 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -5,13 +5,18 @@ export default defineConfig({ coverage: { enabled: true, reporter: ['html', 'json-summary', 'json'], - all: false, - reportOnFailure: true + reportOnFailure: true, + include: ['projects/**/*.ts', 'packages/**/*.ts'], + cleanOnRerun: false }, outputFile: 'test-results.json', - setupFiles: ['./test/setup.ts'], - include: ['./test/test.ts', './test/cases/**/*.test.ts'], - testTimeout: 5000 + setupFiles: 'test/setup.ts', + globalSetup: 'test/globalSetup.ts', + fileParallelism: false, + pool: 'threads', + include: ['test/test.ts', 'test/cases/**/*.test.ts', 'projects/app/test/**/*.test.ts'], + testTimeout: 5000, + reporters: ['github-actions', 'default'] }, resolve: { alias: {