From 73c745652477bf8d9fbfff6d12f20bfc7001eb72 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Wed, 20 Aug 2025 10:50:45 +0800 Subject: [PATCH 01/17] refactor: permission update conflict check function --- .../support/permission/collaborator.d.ts | 1 + packages/global/support/permission/type.d.ts | 1 + packages/global/support/permission/utils.ts | 101 ++++++------ packages/service/common/mongo/index.ts | 27 ++++ .../service/support/permission/controller.ts | 2 +- .../support/permission/inheritPermission.ts | 144 +++++++++++++++--- packages/service/support/permission/schema.ts | 12 +- .../global/support/permission/common.test.ts | 27 +++- .../global/support/permission/utils.test.ts | 81 ++++++++++ 9 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 test/cases/global/support/permission/utils.test.ts diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 150864c5d3f3..240fe350fb71 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -6,6 +6,7 @@ import type { PermissionValueType } from './type'; export type CollaboratorItemType = { teamId: string; permission: Permission; + selfPermission?: Permission; name: string; avatar: string; } & RequireOnlyOne<{ diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.d.ts index db0a085e635c..c1e5521c75c3 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.d.ts @@ -64,6 +64,7 @@ export type ResourcePermissionType = { teamId: string; resourceType: ResourceType; permission: PermissionValueType; + selfPermission?: PermissionValueType; resourceId: string; } & RequireOnlyOne<{ tmbId: string; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index b39900f0630a..e31d2d940444 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,48 +1,11 @@ +import type { CollaboratorItemType } from './collaborator'; import { type PermissionValueType } from './type'; -import { NullRoleVal, PermissionTypeEnum } from './constant'; -import type { Permission } from './controller'; - -/* team public source, or owner source in team */ -export function mongoRPermission({ - teamId, - tmbId, - permission -}: { - teamId: string; - tmbId: string; - permission: Permission; -}) { - if (permission.isOwner) { - return { - teamId - }; - } - return { - teamId, - $or: [{ permission: PermissionTypeEnum.public }, { tmbId }] - }; -} -export function mongoOwnerPermission({ teamId, tmbId }: { teamId: string; tmbId: string }) { - return { - teamId, - tmbId - }; -} - -// return permission-related schema to define the schema of resources -export function getPermissionSchema(defaultPermission: PermissionValueType = NullRoleVal) { - return { - defaultPermission: { - type: Number, - default: defaultPermission - }, - inheritPermission: { - type: Boolean, - default: true - } - }; -} - +/** + * Sum the permission value. + * If no permission value is provided, return undefined to fallback to default value. + * @param per permission value (number) + * @returns sum of permission value + */ export const sumPer = (...per: PermissionValueType[]) => { if (per.length === 0) { // prevent sum 0 value, to fallback to default value @@ -50,3 +13,53 @@ export const sumPer = (...per: PermissionValueType[]) => { } return per.reduce((acc, cur) => acc | cur, 0); }; + +/** + * Check if the update cause conflict (need to remove inheritance permission). + * Conflict condition: + * The updated collaborator is a parent collaborator. + * @param parentClbs parent collaborators + * @param oldChildClbs old child collaborators + * @param newChildClbs new child collaborators + */ +export const checkRoleUpdateConflict = ({ + parentClbs, + oldChildClbs, + newChildClbs +}: { + parentClbs: CollaboratorItemType[]; + oldChildClbs: CollaboratorItemType[]; + newChildClbs: CollaboratorItemType[]; +}): boolean => { + if (parentClbs.length === 0) { + return false; + } + + // 1. find out which collaborator is changed + // Use a Map for faster lookup by teamId + const [oldClbRoleMap, parentClbRoleMap] = [ + new Map(oldChildClbs.map((clb) => [clb.tmbId || clb.groupId || clb.orgId, clb.permission])), + new Map(parentClbs.map((clb) => [clb.tmbId || clb.groupId || clb.orgId, clb.permission])) + ]; + + for (const newClb of newChildClbs) { + const key = newClb.tmbId || newClb.groupId || newClb.orgId; + if (!key) continue; + + const oldPermission = oldClbRoleMap.get(key); + + if (oldPermission === newClb.permission) continue; + + const changedPermission = + oldPermission !== undefined + ? oldPermission.role ^ newClb.permission.role + : newClb.permission.role; + + const parentPermission = parentClbRoleMap.get(key); + if (parentPermission && (changedPermission & parentPermission.role) !== 0) { + return true; + } + } + + return false; +}; diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 0abb1e615f36..ceb9d340fcc3 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -2,6 +2,7 @@ import { isTestEnv } from '@fastgpt/global/common/system/constants'; import { addLog } from '../../common/system/log'; import type { Model } from 'mongoose'; import mongoose, { Mongoose } from 'mongoose'; +import { objectIdToStringPlugin } from './plugin'; export default mongoose; export * from 'mongoose'; @@ -64,6 +65,32 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { } next(); }); + + schema.post(/^find/, function (docs) { + if (!docs) return; + + const convertObjectIds = (obj: any) => { + if (!obj) return; + + // Convert _id + if (obj._id && obj._id.toString) { + obj._id = obj._id.toString(); + } + + // Convert other ObjectId fields + Object.keys(obj).forEach((key) => { + if (obj[key] && obj[key]._bsontype === 'ObjectId') { + obj[key] = obj[key].toString(); + } + }); + }; + + if (Array.isArray(docs)) { + docs.forEach((doc) => convertObjectIds(doc)); + } else { + convertObjectIds(docs); + } + }); }); return schema; diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 96279c472630..6a5c115b1c59 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -128,7 +128,7 @@ export async function getResourceClbsAndGroups({ ).lean(); } -export const getClbsAndGroupsWithInfo = async ({ +export const getClbsWithInfo = async ({ resourceId, resourceType, teamId diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 2ee7d93719d3..3ca84dbe7467 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,11 +1,21 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { + NullPermissionVal, + NullRoleVal, + type PerResourceTypeEnum +} from '@fastgpt/global/support/permission/constant'; +import type { + PermissionValueType, + ResourcePermissionType, + ResourceType +} from '@fastgpt/global/support/permission/type'; import type { ClientSession, Model } from 'mongoose'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { getResourceClbsAndGroups } from './controller'; import { MongoResourcePermission } from './schema'; +import type { AnyBulkWriteOperation } from 'common/mongo'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -21,7 +31,9 @@ export type UpdateCollaboratorItem = { orgId: string; }>; -// sync the permission to all children folders. +/** + * sync the permission to all children folders. + */ export async function syncChildrenPermission({ resource, folderTypeList, @@ -46,15 +58,16 @@ export async function syncChildrenPermission({ }) { // only folder has permission const isFolder = folderTypeList.includes(resource.type); + const teamId = resource.teamId; if (!isFolder) return; // get all folders and the resource permission of the app - const allFolders = await resourceModel + const allResources = await resourceModel .find( { - teamId: resource.teamId, - type: { $in: folderTypeList }, + teamId, + // type: { $in: folderTypeList }, inheritPermission: true }, '_id parentId' @@ -62,35 +75,122 @@ export async function syncChildrenPermission({ .lean() .session(session); - // bfs to get all children + const allClbs = await MongoResourcePermission.find({ + resourceType, + teamId + }) + .lean() + .session(session); + + const resourceMap = new Map(); + const parentChildrenMap = new Map(); + const resourceIdPermissionMap = new Map(); + + // init the map + allResources.forEach((resource) => { + resourceMap.set(resource._id, resource); + const parentId = String(resource.parentId); + if (!parentChildrenMap.has(parentId)) { + parentChildrenMap.set(parentId, []); + } + parentChildrenMap.get(parentId)!.push(resource); + }); + + allClbs.forEach((clb) => { + const resourceId = String(clb.resourceId); + if (!resourceIdPermissionMap.has(resourceId)) { + resourceIdPermissionMap.set(resourceId, []); + } + resourceIdPermissionMap.get(resourceId)!.push(clb); + }); + + // BFS to get all children const queue = [String(resource._id)]; const children: string[] = []; + const visited = new Set(); + while (queue.length) { - const parentId = queue.shift(); - const folderChildren = allFolders.filter( - (folder) => String(folder.parentId) === String(parentId) - ); - children.push(...folderChildren.map((folder) => folder._id)); - queue.push(...folderChildren.map((folder) => folder._id)); + const parentId = String(queue.shift()!); + if (visited.has(parentId)) continue; + visited.add(parentId); + const _children = parentChildrenMap.get(parentId) || []; + children.push(..._children.map((child) => child._id)); + queue.push(..._children.map((child) => child._id)); } if (!children.length) return; + const ops: AnyBulkWriteOperation[] = []; + // sync the resource permission if (collaborators) { // Update the collaborators of all children for await (const childId of children) { - await syncCollaborators({ - resourceType, - session, - collaborators, - teamId: resource.teamId, - resourceId: childId - }); + const childResource = resourceMap.get(childId)!; + const parentResource = childResource.parentId + ? resourceMap.get(String(childResource.parentId)) + : undefined; + const childClbs = resourceIdPermissionMap.get(childId) || []; + const parentClbs = parentResource + ? resourceIdPermissionMap.get(String(parentResource._id)) || [] + : []; + + console.log(childResource, parentResource, childClbs, parentClbs); + if (parentResource) { + for (const parentClb of parentClbs) { + const childClb = childClbs.find( + (clb) => + String(clb.tmbId) === String(parentClb.tmbId) || + String(clb.groupId) === String(parentClb.groupId) || + String(clb.orgId) === String(parentClb.orgId) + ); + if (childClb) { + // child has the same collaborator, add the permission on it. + ops.push({ + updateOne: { + filter: { + resourceId: childId, + resourceType, + teamId + }, + update: { + permission: sumPer(parentClb.permission, childClb.permission), + selfPermission: childClb.permission // save the raw permission + } + } + }); + } else { + // child has no collaborator, add the permission on it. + ops.push({ + updateOne: { + filter: { + resourceId: childId, + resourceType, + teamId + }, + update: { + permission: parentClb.permission, + selfPermission: NullRoleVal // the raw permission is 0 + } + } + }); + } + } + // only children need to be updated + // await syncCollaborators({ + // resourceType, + // session, + // collaborators, + // teamId: resource.teamId, + // resourceId: childId + // }); + } } } + + await MongoResourcePermission.bulkWrite(ops, { session }); } -/* Resume the inherit permission of the resource. +/** Resume the inherit permission of the resource. 1. Folder: Sync parent's defaultPermission and clbs, and sync its children. 2. Resource: Sync parent's defaultPermission, and delete all its clbs. */ @@ -162,7 +262,7 @@ export async function resumeInheritPermission({ } } -/* +/** Delete all the collaborators and then insert the new collaborators. */ export async function syncCollaborators({ diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index ae53c7da1d2e..388482b66fcd 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -34,11 +34,21 @@ export const ResourcePermissionSchema = new Schema({ enum: Object.values(PerResourceTypeEnum), required: true }, + /** + * The **Role** of the object to the resource. + */ permission: { type: Number, required: true }, - // Resrouce ID: App or DataSet or any other resource type. + /** + * Optional. Only be set when the resource is *inherited* from the parent resource. + * For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`. + */ + selfPermission: { + type: Number + }, + // Resource ID: App or DataSet or any other resource type. // It is null if the resourceType is team. resourceId: { type: Schema.Types.ObjectId diff --git a/test/cases/global/support/permission/common.test.ts b/test/cases/global/support/permission/common.test.ts index b65577662ce3..09d1c437d200 100644 --- a/test/cases/global/support/permission/common.test.ts +++ b/test/cases/global/support/permission/common.test.ts @@ -1,5 +1,10 @@ -import { CommonPerList, CommonRoleList } from '@fastgpt/global/support/permission/constant'; +import { + CommonPerList, + CommonRoleList, + OwnerRoleVal +} from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { sumPer } from '@fastgpt/global/support/permission/utils'; import { describe, expect, it } from 'vitest'; describe('Permission Helper Class Test', () => { @@ -14,6 +19,22 @@ describe('Permission Helper Class Test', () => { permission.removeRole(CommonRoleList.read.value); expect(permission.checkPer(CommonPerList.manage)).toBe(true); }); + it('Owner Permission Test', () => { + const permission = new Permission({ isOwner: true }); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + expect(permission.checkPer(CommonPerList.read)).toBe(true); + expect(permission.checkPer(CommonPerList.write)).toBe(true); + expect(permission.checkPer(CommonPerList.manage)).toBe(true); + expect(permission.checkRole(CommonRoleList.read.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.manage.value)).toBe(true); + expect(permission.checkRole(CommonRoleList.write.value)).toBe(true); + expect(permission.checkRole(OwnerRoleVal)).toBe(true); + + permission.addRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + permission.removeRole(CommonRoleList.read.value); + expect(permission.checkPer(CommonPerList.owner)).toBe(true); + }); }); describe('Tool Functions', () => { @@ -22,7 +43,9 @@ describe('Tool Functions', () => { expect(sumPer(0b000, 0b000)).toBe(0b000); expect(sumPer(0b100, 0b001)).toBe(0b101); expect(sumPer(0b111, 0b010)).toBe(0b111); - expect(sumPer(sumPer(0b001, 0b010), 0b100)).toBe(0b111); + expect(sumPer(sumPer(0b001, 0b010) as PermissionValueType, 0b100)).toBe(0b111); expect(sumPer(0b10000000, 0b01000000)).toBe(0b11000000); + expect(sumPer()).toBe(undefined); + expect(sumPer() || 0b111).toBe(0b111); }); }); diff --git a/test/cases/global/support/permission/utils.test.ts b/test/cases/global/support/permission/utils.test.ts new file mode 100644 index 000000000000..b0913b45f903 --- /dev/null +++ b/test/cases/global/support/permission/utils.test.ts @@ -0,0 +1,81 @@ +import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { checkRoleUpdateConflict } from '@fastgpt/global/support/permission/utils'; +import { describe, expect, it } from 'vitest'; + +const fakeClb = ({ + role, + selfRole, + tmbId +}: { + role: PermissionValueType; + selfRole?: PermissionValueType; + tmbId: string; +}): CollaboratorItemType => ({ + avatar: 'fakeAvatar', + name: 'fakeName', + permission: new Permission({ role: role }), + selfPermission: selfRole ? new Permission({ role: selfRole }) : undefined, + teamId: 'fakeTeamId', + tmbId +}); + +describe('Test checkRoleUpdateConflict', () => { + it('There is no any old collaborator, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + oldChildClbs: [], + newChildClbs: [fakeClb({ role: 0b001, tmbId: 'fakeTmbId1' })] + }); + expect(result).toBe(false); + }); + it('There is no parent collaborator, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [], + oldChildClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], + newChildClbs: [fakeClb({ role: 0b001, tmbId: 'fakeTmbId2' })] + }); + expect(result).toBe(false); + }); + it("Edit parent's permission, should return true", () => { + const result = checkRoleUpdateConflict({ + parentClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], + oldChildClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], + newChildClbs: [fakeClb({ role: 0b010, tmbId: 'fakeTmbId1' })] + }); + expect(result).toBe(true); + }); + it("Edit permission but parent's permission bit is not set, should return false", () => { + const result = checkRoleUpdateConflict({ + parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + oldChildClbs: [fakeClb({ role: 0b1111, tmbId: 'fakeTmbId1' })], + newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })] + }); + expect(result).toBe(false); + }); + it('add new clb, should return false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + oldChildClbs: [fakeClb({ role: 0b1111, tmbId: 'fakeTmbId1' })], + newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId2' })] + }); + expect(result).toBe(false); + }); + it('add clb, no oldChildClbs', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + oldChildClbs: [], + newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })] + }); + expect(result).toBe(true); + }); + it('add clb, no oldChildClbs, false', () => { + const result = checkRoleUpdateConflict({ + parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + oldChildClbs: [], + newChildClbs: [fakeClb({ role: 0b0110, tmbId: 'fakeTmbId1' })] + }); + expect(result).toBe(false); + }); +}); From 105b60e9db93ea143d8d396809e754be9effb272 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Thu, 21 Aug 2025 11:18:41 +0800 Subject: [PATCH 02/17] refactor(permission): app collaborator update api --- .../support/permission/collaborator.d.ts | 39 ++++---- .../support/permission/{type.d.ts => type.ts} | 10 +- packages/global/support/permission/utils.ts | 92 +++++++++++++++---- packages/service/common/mongo/index.ts | 1 + .../service/support/permission/controller.ts | 10 +- .../support/permission/inheritPermission.ts | 34 ++----- .../global/support/permission/utils.test.ts | 55 ++++------- 7 files changed, 133 insertions(+), 108 deletions(-) rename packages/global/support/permission/{type.d.ts => type.ts} (84%) diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 240fe350fb71..33e325e339cf 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -1,32 +1,29 @@ +import type { UpdateAppCollaboratorBody } from 'core/app/collaborator'; import type { RequireOnlyOne } from '../../common/type/utils'; import { RequireAtLeastOne } from '../../common/type/utils'; import type { Permission } from './controller'; -import type { PermissionValueType } from './type'; +import type { PermissionValueType, RoleValueType } from './type'; -export type CollaboratorItemType = { +export type CollaboratorIdType = RequireOnlyOne<{ + tmbId: string; + groupId: string; + orgId: string; +}>; + +export type CollaboratorItemDetailType = { teamId: string; permission: Permission; selfPermission?: Permission; name: string; avatar: string; -} & RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; -}>; +} & CollaboratorIdType; -export type UpdateClbPermissionProps = { - members?: string[]; - groups?: string[]; - orgs?: string[]; -} & (addOnly extends true - ? {} - : { - permission: PermissionValueType; - }); +export type CollaboratorItemType = { + permission: PermissionValueType; +} & CollaboratorIdType; -export type DeletePermissionQuery = RequireOnlyOne<{ - tmbId?: string; - groupId?: string; - orgId?: string; -}>; +export type UpdateClbPermissionProps = { + collaborators: CollaboratorItemType[]; +}; + +export type DeletePermissionQuery = CollaboratorIdType; diff --git a/packages/global/support/permission/type.d.ts b/packages/global/support/permission/type.ts similarity index 84% rename from packages/global/support/permission/type.d.ts rename to packages/global/support/permission/type.ts index c1e5521c75c3..66281f3490b4 100644 --- a/packages/global/support/permission/type.d.ts +++ b/packages/global/support/permission/type.ts @@ -1,10 +1,8 @@ import type { UserModelSchema } from '../user/type'; import type { RequireOnlyOne } from '../../common/type/utils'; import type { TeamMemberSchema } from '../user/team/type'; -import { MemberGroupSchemaType } from './memberGroup/type'; -import type { TeamMemberWithUserSchema } from '../user/team/type'; -import type { CommonPerKeyEnum, CommonRoleKeyEnum } from './constant'; -import { AuthUserTypeEnum, type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; +import type { CommonRoleKeyEnum } from './constant'; +import { type CommonPerKeyEnum, type PerResourceTypeEnum } from './constant'; // 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. @@ -18,7 +16,7 @@ export type ResourceType = `${PerResourceTypeEnum}`; /** * Define the roles. Each role is a binary number, only one bit is set to 1. */ -export type RoleListType = Readonly< +export type RoleListType = Readonly< Record< T | CommonRoleKeyEnum, Readonly<{ @@ -43,7 +41,7 @@ export type RoleListType = Readonly< * write: 0b110, // bad, should be 0b010 * } */ -export type PermissionListType = Readonly< +export type PermissionListType = Readonly< Record >; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index e31d2d940444..115b99f16e53 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,4 +1,9 @@ -import type { CollaboratorItemType } from './collaborator'; +import { getClientBuildManifest } from 'next/dist/client/route-loader'; +import type { + CollaboratorIdType, + CollaboratorItemDetailType, + CollaboratorItemType +} from './collaborator'; import { type PermissionValueType } from './type'; /** * Sum the permission value. @@ -35,31 +40,84 @@ export const checkRoleUpdateConflict = ({ return false; } - // 1. find out which collaborator is changed // Use a Map for faster lookup by teamId const [oldClbRoleMap, parentClbRoleMap] = [ - new Map(oldChildClbs.map((clb) => [clb.tmbId || clb.groupId || clb.orgId, clb.permission])), - new Map(parentClbs.map((clb) => [clb.tmbId || clb.groupId || clb.orgId, clb.permission])) + new Map(oldChildClbs.map((clb) => [getCollaboratorId(clb), clb])), + new Map(parentClbs.map((clb) => [getCollaboratorId(clb), clb])) ]; - for (const newClb of newChildClbs) { - const key = newClb.tmbId || newClb.groupId || newClb.orgId; - if (!key) continue; + const changedClbs = getChangedCollaborators({ + newClbs: newChildClbs, + oldClbs: oldChildClbs + }); - const oldPermission = oldClbRoleMap.get(key); + console.log('changedClbs', changedClbs); + console.log('parentClbRoleMap', parentClbRoleMap); + for (const changedClb of changedClbs) { + const parent = parentClbRoleMap.get(getCollaboratorId(changedClb)); + if (parent && (changedClb.changedRole & parent.permission) !== 0) { + return true; + } + } - if (oldPermission === newClb.permission) continue; + return false; +}; - const changedPermission = - oldPermission !== undefined - ? oldPermission.role ^ newClb.permission.role - : newClb.permission.role; +/** + * Get changed collaborators. + * return empty array if all collaborators are unchanged. + * for each return item: { + * id: string; // collaborator id + * changedRole: number; // set bit means the role is changed + * } + * @param param0 + */ +export const getChangedCollaborators = ({ + oldClbs, + newClbs +}: { + oldClbs: CollaboratorItemType[]; + newClbs: CollaboratorItemType[]; +}) => { + if (oldClbs.length === 0) { + return newClbs.map((clb) => ({ + ...clb, + changedRole: clb.permission + })); + } + const oldClbsMap = new Map(oldClbs.map((clb) => [getCollaboratorId(clb), clb])); + const changedClbs = []; + for (const newClb of newClbs) { + const oldClb = oldClbsMap.get(getCollaboratorId(newClb)); + if (!oldClb) { + changedClbs.push({ + ...newClb, + changedRole: newClb.permission + }); + continue; + } + const changedRole = oldClb.permission ^ newClb.permission; + if (changedRole) { + changedClbs.push({ + ...newClb, + changedRole + }); + } + } - const parentPermission = parentClbRoleMap.get(key); - if (parentPermission && (changedPermission & parentPermission.role) !== 0) { - return true; + for (const oldClb of oldClbs) { + const newClb = newClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb)); + if (!newClb) { + changedClbs.push({ + ...oldClb, + changedRole: oldClb.permission, + deleted: true + }); } } - return false; + return changedClbs; }; + +export const getCollaboratorId = (clb: CollaboratorItemType) => + (clb.tmbId || clb.groupId || clb.orgId)!; diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index ceb9d340fcc3..0def9225fb05 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -66,6 +66,7 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { next(); }); + // Convert _id to string schema.post(/^find/, function (docs) { if (!docs) return; diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 6a5c115b1c59..e002ce806664 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -22,6 +22,10 @@ import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; import { authUserSession } from '../user/session'; import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { + CollaboratorItemDetailType, + SimpleCollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -106,7 +110,7 @@ export const getResourcePermission = async ({ return sumPer(...groupPers, ...orgPers); }; -export async function getResourceClbsAndGroups({ +export async function getResourceClbs({ resourceId, resourceType, teamId, @@ -115,7 +119,7 @@ export async function getResourceClbsAndGroups({ resourceId: ParentIdType; resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; teamId: string; - session: ClientSession; + session?: ClientSession; }) { return MongoResourcePermission.find( { @@ -124,7 +128,7 @@ export async function getResourceClbsAndGroups({ teamId }, undefined, - { session } + { ...(session ? { session } : {}) } ).lean(); } diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 3ca84dbe7467..92e5ad5611f7 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,21 +1,13 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import { - NullPermissionVal, - NullRoleVal, - type PerResourceTypeEnum -} from '@fastgpt/global/support/permission/constant'; -import type { - PermissionValueType, - ResourcePermissionType, - ResourceType -} from '@fastgpt/global/support/permission/type'; +import { NullRoleVal, type PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import type { ClientSession, Model } from 'mongoose'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from './controller'; +import { getResourceClbs } from './controller'; import { MongoResourcePermission } from './schema'; import type { AnyBulkWriteOperation } from 'common/mongo'; import { sumPer } from '@fastgpt/global/support/permission/utils'; +import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -23,13 +15,6 @@ export type SyncChildrenPermissionResourceType = { teamId: string; parentId?: ParentIdType; }; -export type UpdateCollaboratorItem = { - permission: PermissionValueType; -} & RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; -}>; /** * sync the permission to all children folders. @@ -54,20 +39,20 @@ export async function syncChildrenPermission({ // should be provided when inheritPermission is true session: ClientSession; - collaborators?: UpdateCollaboratorItem[]; + collaborators?: CollaboratorItemType[]; }) { // only folder has permission const isFolder = folderTypeList.includes(resource.type); const teamId = resource.teamId; + // If the 'root' is not a folder, which means the 'root' has no children, no need to sync. if (!isFolder) return; - // get all folders and the resource permission of the app + // get all the resource permission of the app const allResources = await resourceModel .find( { teamId, - // type: { $in: folderTypeList }, inheritPermission: true }, '_id parentId' @@ -134,7 +119,6 @@ export async function syncChildrenPermission({ ? resourceIdPermissionMap.get(String(parentResource._id)) || [] : []; - console.log(childResource, parentResource, childClbs, parentClbs); if (parentResource) { for (const parentClb of parentClbs) { const childClb = childClbs.find( @@ -223,7 +207,7 @@ export async function resumeInheritPermission({ // Folder resource, need to sync children if (isFolder) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ + const parentClbsAndGroups = await getResourceClbs({ resourceId: resource.parentId, teamId: resource.teamId, resourceType, @@ -275,7 +259,7 @@ export async function syncCollaborators({ resourceType: PerResourceTypeEnum; teamId: string; resourceId: string; - collaborators: UpdateCollaboratorItem[]; + collaborators: CollaboratorItemType[]; session: ClientSession; }) { await MongoResourcePermission.deleteMany( diff --git a/test/cases/global/support/permission/utils.test.ts b/test/cases/global/support/permission/utils.test.ts index b0913b45f903..1b55822fcdc7 100644 --- a/test/cases/global/support/permission/utils.test.ts +++ b/test/cases/global/support/permission/utils.test.ts @@ -1,80 +1,63 @@ -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; -import { Permission } from '@fastgpt/global/support/permission/controller'; -import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; import { checkRoleUpdateConflict } from '@fastgpt/global/support/permission/utils'; import { describe, expect, it } from 'vitest'; -const fakeClb = ({ - role, - selfRole, - tmbId -}: { - role: PermissionValueType; - selfRole?: PermissionValueType; - tmbId: string; -}): CollaboratorItemType => ({ - avatar: 'fakeAvatar', - name: 'fakeName', - permission: new Permission({ role: role }), - selfPermission: selfRole ? new Permission({ role: selfRole }) : undefined, - teamId: 'fakeTeamId', - tmbId -}); - describe('Test checkRoleUpdateConflict', () => { it('There is no any old collaborator, should return false', () => { const result = checkRoleUpdateConflict({ parentClbs: [], oldChildClbs: [], - newChildClbs: [fakeClb({ role: 0b001, tmbId: 'fakeTmbId1' })] + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); }); it('There is no parent collaborator, should return false', () => { const result = checkRoleUpdateConflict({ parentClbs: [], - oldChildClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], - newChildClbs: [fakeClb({ role: 0b001, tmbId: 'fakeTmbId2' })] + oldChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId2' }] }); expect(result).toBe(false); }); it("Edit parent's permission, should return true", () => { const result = checkRoleUpdateConflict({ - parentClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], - oldChildClbs: [fakeClb({ role: 0b011, tmbId: 'fakeTmbId1' })], - newChildClbs: [fakeClb({ role: 0b010, tmbId: 'fakeTmbId1' })] + parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + oldChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b010, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(true); }); it("Edit permission but parent's permission bit is not set, should return false", () => { const result = checkRoleUpdateConflict({ - parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], - oldChildClbs: [fakeClb({ role: 0b1111, tmbId: 'fakeTmbId1' })], - newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })] + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldChildClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); }); it('add new clb, should return false', () => { const result = checkRoleUpdateConflict({ - parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], - oldChildClbs: [fakeClb({ role: 0b1111, tmbId: 'fakeTmbId1' })], - newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId2' })] + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], + oldChildClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + newChildClbs: [ + { permission: 0b1111, tmbId: 'fakeTmbId1' }, + { permission: 0b1001, tmbId: 'fakeTmbId2' } + ] }); expect(result).toBe(false); }); it('add clb, no oldChildClbs', () => { const result = checkRoleUpdateConflict({ - parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], oldChildClbs: [], - newChildClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })] + newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(true); }); it('add clb, no oldChildClbs, false', () => { const result = checkRoleUpdateConflict({ - parentClbs: [fakeClb({ role: 0b1001, tmbId: 'fakeTmbId1' })], + parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], oldChildClbs: [], - newChildClbs: [fakeClb({ role: 0b0110, tmbId: 'fakeTmbId1' })] + newChildClbs: [{ permission: 0b0110, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); }); From cd55bb184b059a8ae11a8b89abf6dcca756233ef Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Fri, 22 Aug 2025 08:58:24 +0800 Subject: [PATCH 03/17] refactor(permission): support app update collaborator --- packages/global/support/permission/type.ts | 4 +- packages/global/support/permission/utils.ts | 2 +- .../support/permission/inheritPermission.ts | 217 +++++++----- packages/service/support/permission/utils.ts | 9 + .../permission/ConfigPerModal/index.tsx | 28 +- .../permission/MemberManager/ManageModal.tsx | 3 + .../MemberManager/MemberItemCard.tsx | 72 ++-- .../permission/MemberManager/MemberModal.tsx | 324 +++++++++--------- .../permission/MemberManager/RoleSelect.tsx | 14 +- .../permission/MemberManager/context.tsx | 71 +--- .../pageComponents/dashboard/apps/List.tsx | 11 +- .../app/src/web/core/app/api/collaborator.ts | 7 +- .../src/web/core/dataset/api/collaborator.ts | 7 +- .../src/web/support/user/team/group/api.ts | 6 +- 14 files changed, 398 insertions(+), 377 deletions(-) create mode 100644 packages/service/support/permission/utils.ts diff --git a/packages/global/support/permission/type.ts b/packages/global/support/permission/type.ts index 66281f3490b4..0c3df5cc3d7a 100644 --- a/packages/global/support/permission/type.ts +++ b/packages/global/support/permission/type.ts @@ -16,7 +16,7 @@ export type ResourceType = `${PerResourceTypeEnum}`; /** * Define the roles. Each role is a binary number, only one bit is set to 1. */ -export type RoleListType = Readonly< +export type RoleListType = Readonly< Record< T | CommonRoleKeyEnum, Readonly<{ @@ -41,7 +41,7 @@ export type RoleListType = Readonly< * write: 0b110, // bad, should be 0b010 * } */ -export type PermissionListType = Readonly< +export type PermissionListType = Readonly< Record >; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index 115b99f16e53..4fa922f8ac6c 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -119,5 +119,5 @@ export const getChangedCollaborators = ({ return changedClbs; }; -export const getCollaboratorId = (clb: CollaboratorItemType) => +export const getCollaboratorId = (clb: CollaboratorIdType) => (clb.tmbId || clb.groupId || clb.orgId)!; diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 92e5ad5611f7..fdf7e2529963 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,13 +1,18 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; -import { NullRoleVal, type PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { + NullPermissionVal, + NullRoleVal, + type PerResourceTypeEnum +} from '@fastgpt/global/support/permission/constant'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; -import type { ClientSession, Model } from 'mongoose'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { getResourceClbs } from './controller'; import { MongoResourcePermission } from './schema'; -import type { AnyBulkWriteOperation } from 'common/mongo'; -import { sumPer } from '@fastgpt/global/support/permission/utils'; +import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; +import { Types } from '../../common/mongo'; +import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { pickCollaboratorIdFields } from './utils'; export type SyncChildrenPermissionResourceType = { _id: string; @@ -41,6 +46,7 @@ export async function syncChildrenPermission({ collaborators?: CollaboratorItemType[]; }) { + console.log('collaborators', collaborators); // only folder has permission const isFolder = folderTypeList.includes(resource.type); const teamId = resource.teamId; @@ -67,9 +73,10 @@ export async function syncChildrenPermission({ .lean() .session(session); + /** ResourceMap */ const resourceMap = new Map(); + /** parentChildrenMap */ const parentChildrenMap = new Map(); - const resourceIdPermissionMap = new Map(); // init the map allResources.forEach((resource) => { @@ -81,97 +88,147 @@ export async function syncChildrenPermission({ parentChildrenMap.get(parentId)!.push(resource); }); - allClbs.forEach((clb) => { - const resourceId = String(clb.resourceId); - if (!resourceIdPermissionMap.has(resourceId)) { - resourceIdPermissionMap.set(resourceId, []); + /** resourceIdPermissionMap + * save the clb virtual state, not the real state at present in the DB. + */ + const resourceIdClbMap = new Map(); + + // Initialize the resourceIdPermissionMap + // 1. add `root` clbs first + resourceIdClbMap.set( + resource._id, + collaborators?.map((clb) => ({ + ...clb, + teamId: resource.teamId, + resourceId: resource._id, + resourceType: resourceType + })) ?? [] + ); + // 2. add the clbs what we have now according to allClbs + for (const clb of allClbs) { + const resourceId = clb.resourceId; + if (resourceId === resource._id) continue; + const arr = resourceIdClbMap.get(resourceId); + if (!arr) { + resourceIdClbMap.set(resourceId, [clb]); + } else { + arr.push(clb); } - resourceIdPermissionMap.get(resourceId)!.push(clb); - }); + } // BFS to get all children const queue = [String(resource._id)]; - const children: string[] = []; - const visited = new Set(); + const ops: AnyBulkWriteOperation[] = []; while (queue.length) { - const parentId = String(queue.shift()!); - if (visited.has(parentId)) continue; - visited.add(parentId); + const parentId = String(queue.shift()); const _children = parentChildrenMap.get(parentId) || []; - children.push(..._children.map((child) => child._id)); - queue.push(..._children.map((child) => child._id)); - } - if (!children.length) return; + if (_children.length === 0) continue; + for (const child of _children) { + // 1. get parent's permission and what permission I have. + const parentClbs = resourceIdClbMap.get(String(child.parentId)) || []; + console.log('parentClbs', parentClbs); + const myClbs = resourceIdClbMap.get(String(child._id)) || []; + console.log('myClbs', myClbs); + // 2. merge the permission and generate operations for mongo. + // rules: + // i. if parent has and I have not, get the clb. + // ii. if parent has not and I have. If my clb has no selfPermission, + // it should be removed. Otherwise, it should be remained. + // iii. if we both have, get the sumPer. - const ops: AnyBulkWriteOperation[] = []; + const bothHaveClbs = parentClbs + .filter((clb) => + myClbs.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) + ) + .map((clb) => ({ + ...clb, + permission: sumPer( + clb.permission, + myClbs.find((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb))!.permission + )! + })); - // sync the resource permission - if (collaborators) { - // Update the collaborators of all children - for await (const childId of children) { - const childResource = resourceMap.get(childId)!; - const parentResource = childResource.parentId - ? resourceMap.get(String(childResource.parentId)) - : undefined; - const childClbs = resourceIdPermissionMap.get(childId) || []; - const parentClbs = parentResource - ? resourceIdPermissionMap.get(String(parentResource._id)) || [] - : []; + const parentHasAndIHaveNot = parentClbs.filter( + (clb) => !myClbs.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) + ); - if (parentResource) { - for (const parentClb of parentClbs) { - const childClb = childClbs.find( - (clb) => - String(clb.tmbId) === String(parentClb.tmbId) || - String(clb.groupId) === String(parentClb.groupId) || - String(clb.orgId) === String(parentClb.orgId) - ); - if (childClb) { - // child has the same collaborator, add the permission on it. - ops.push({ - updateOne: { - filter: { - resourceId: childId, - resourceType, - teamId - }, - update: { - permission: sumPer(parentClb.permission, childClb.permission), - selfPermission: childClb.permission // save the raw permission - } - } - }); - } else { - // child has no collaborator, add the permission on it. - ops.push({ - updateOne: { - filter: { - resourceId: childId, - resourceType, - teamId - }, - update: { - permission: parentClb.permission, - selfPermission: NullRoleVal // the raw permission is 0 - } - } - }); + const IHaveAndParentHasNot = myClbs.filter( + (clb) => + !parentClbs.some((parentClb) => getCollaboratorId(clb) === getCollaboratorId(parentClb)) + ); + + console.log('bothHaveClbs', bothHaveClbs); + console.log('parentHasAndIHaveNot', parentHasAndIHaveNot); + console.log('IHaveAndParentHasNot', IHaveAndParentHasNot); + + // generate ops + // i. + for (const clb of parentHasAndIHaveNot) { + ops.push({ + updateOne: { + filter: { + resourceId: child._id, + resourceType, + teamId, + ...pickCollaboratorIdFields(clb) + }, + update: { + permission: clb.permission, + selfPermission: NullPermissionVal + }, + upsert: true } + }); + } + + // ii. + for (const clb of IHaveAndParentHasNot) { + if (clb.selfPermission === NullPermissionVal) { + ops.push({ + deleteOne: { + filter: { + resourceId: child._id, + resourceType, + teamId, + ...pickCollaboratorIdFields(clb) + } + } + }); } - // only children need to be updated - // await syncCollaborators({ - // resourceType, - // session, - // collaborators, - // teamId: resource.teamId, - // resourceId: childId - // }); } + + // iii. + for (const clb of bothHaveClbs) { + ops.push({ + updateOne: { + filter: { + resourceId: child._id, + resourceType, + teamId, + ...pickCollaboratorIdFields(clb) + }, + update: { + permission: clb.permission + // do not update selfPermission + }, + upsert: true + } + }); + } + // 3. save the permission status for my children + resourceIdClbMap.set(child._id, [ + ...parentHasAndIHaveNot, + ...bothHaveClbs, + ...IHaveAndParentHasNot.filter((clb) => clb.selfPermission !== NullPermissionVal) + ]); + // 4. add myself to the queue + queue.push(child._id); } } - + console.log(ops, JSON.stringify(ops, null, 2)); await MongoResourcePermission.bulkWrite(ops, { session }); + return; } /** Resume the inherit permission of the resource. diff --git a/packages/service/support/permission/utils.ts b/packages/service/support/permission/utils.ts new file mode 100644 index 000000000000..eb4a70b80fe3 --- /dev/null +++ b/packages/service/support/permission/utils.ts @@ -0,0 +1,9 @@ +import type { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator'; + +export const pickCollaboratorIdFields = (clb: CollaboratorIdType) => { + return { + ...(clb.tmbId && { tmbId: clb.tmbId }), + ...(clb.groupId && { groupId: clb.groupId }), + ...(clb.orgId && { orgId: clb.orgId }) + }; +}; diff --git a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx index bd5ae5434ba6..e0907cda90bc 100644 --- a/projects/app/src/components/support/permission/ConfigPerModal/index.tsx +++ b/projects/app/src/components/support/permission/ConfigPerModal/index.tsx @@ -67,7 +67,7 @@ const ConfigPerModal = ({ isInheritPermission={isInheritPermission} hasParent={hasParent} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx index cf84645e7428..984d524692a7 100644 --- a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx @@ -1,3 +1,6 @@ +/** + * @deprecated + */ import { useUserStore } from '@/web/support/user/useUserStore'; import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index 5811fd7a6702..04a2684f3991 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -1,41 +1,40 @@ import React from 'react'; import { useTranslation } from 'next-i18next'; -import { Box, Checkbox, HStack, VStack } from '@chakra-ui/react'; +import { Box, Checkbox, Flex, HStack, VStack } from '@chakra-ui/react'; import Avatar from '@fastgpt/web/components/common/Avatar'; import RoleTags from './RoleTags'; import type { RoleValueType } 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'; +import RoleSelect from './RoleSelect'; +import { ChevronDownIcon } from '@chakra-ui/icons'; +import { OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; function MemberItemCard({ avatar, key, - onChange: _onChange, + onChange, isChecked, onDelete, name, role, orgs, - addOnly, - rightSlot + rightSlot, + onRoleChange }: { avatar: string; key: string; - onChange: () => void; + onChange?: () => void; + onRoleChange?: (role: RoleValueType) => void; isChecked?: boolean; onDelete?: () => void; name: string; role?: RoleValueType; - addOnly?: boolean; orgs?: string[]; rightSlot?: React.ReactNode; }) { - const isAdded = addOnly && !!role; - const onChange = () => { - if (!isAdded) _onChange(); - }; - const { t } = useTranslation(); + const isDisabled = role === OwnerRoleVal; return ( { + if (isDisabled) return; + onChange?.(); + }} > - {isChecked !== undefined && ( - - )} + {isChecked !== undefined && } @@ -61,18 +62,30 @@ function MemberItemCard({ {orgs && orgs.length > 0 && } - {!isAdded && role && } - {isAdded && ( - - {t('user:team.collaborator.added')} - + {role !== undefined && !!onRoleChange && ( + + {role && ( + + + + )} + + + } + onChange={onRoleChange} + /> )} {onDelete !== undefined && ( { + if (isDisabled) return; + onDelete?.(); + }} /> )} {rightSlot} diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 3755d237e10e..29626151c22a 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -3,18 +3,16 @@ import { getTeamMembers } from '@/web/support/user/team/api'; import { getGroupList } from '@/web/support/user/team/group/api'; import useOrg from '@/web/support/user/team/org/hooks/useOrg'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { ChevronDownIcon } from '@chakra-ui/icons'; -import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter, Text } from '@chakra-ui/react'; +import { Box, Button, Flex, Grid, HStack, ModalBody, ModalFooter } from '@chakra-ui/react'; import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_AVATAR, DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; -import { type UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { type OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; -import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; +import { TeamTmbItemType, type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; @@ -22,28 +20,28 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useTranslation } from 'next-i18next'; -import { type ValueOf } from 'next/dist/shared/lib/constants'; -import { useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { CollaboratorContext } from './context'; import MemberItemCard from './MemberItemCard'; -import RoleSelect from './RoleSelect'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import type { RoleValueType } from '@fastgpt/global/support/permission/type'; +import { Permission } from '@fastgpt/global/support/permission/controller'; +import { getCollaboratorId } from '@fastgpt/global/support/permission/utils'; const HoverBoxStyle = { bgColor: 'myGray.50', cursor: 'pointer' }; -function MemberModal({ - onClose, - addPermissionOnly: addOnly = false -}: { - onClose: () => void; - addPermissionOnly?: boolean; -}) { +function MemberModal({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const { userInfo } = useUserStore(); - const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const collaboratorDetailList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList); + const defaultRole = useContextSelector(CollaboratorContext, (v) => v.defaultRole); const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>(); const { paths, @@ -91,20 +89,11 @@ function MemberModal({ } ); - const [selectedOrgList, setSelectedOrgIdList] = useState([]); - - const [selectedMemberList, setSelectedMemberList] = useState< - Omit[] - >([]); + const [editCollaborators, setCollaboratorList] = useState([]); - const [selectedGroupList, setSelectedGroupList] = useState[]>([]); - const roleList = useContextSelector(CollaboratorContext, (v) => v.roleList); - const getRoleLabelList = useContextSelector(CollaboratorContext, (v) => v.getRoleLabelList); - const [selectedRole, setSelectedRole] = useState(roleList?.read?.value); - const roleLabel = useMemo(() => { - if (selectedRole === undefined) return ''; - return getRoleLabelList(selectedRole!).join('、'); - }, [getRoleLabelList, selectedRole]); + useEffect(() => { + setCollaboratorList(collaboratorDetailList); + }, [collaboratorDetailList]); const onUpdateCollaborators = useContextSelector( CollaboratorContext, @@ -114,11 +103,16 @@ function MemberModal({ const { runAsync: onConfirm, loading: isUpdating } = useRequest2( () => onUpdateCollaborators({ - members: selectedMemberList.map((item) => item.tmbId), - groups: selectedGroupList.map((item) => item._id), - orgs: selectedOrgList.map((item) => item._id), - permission: addOnly ? undefined : selectedRole! - } as UpdateClbPermissionProps>), + collaborators: editCollaborators.map( + (clb) => + ({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, + permission: clb.permission.role + }) as CollaboratorItemType + ) + }), { successToast: t('common:add_success'), onSuccess() { @@ -133,40 +127,13 @@ function MemberModal({ { label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' } ]); - const selectedList = useMemo(() => { - return [ - ...selectedOrgList.map((item) => ({ - id: `org-${item._id}`, - avatar: item.avatar, - name: item.name, - onDelete: () => setSelectedOrgIdList(selectedOrgList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedGroupList.map((item) => ({ - id: `group-${item._id}`, - avatar: item.avatar, - name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name, - onDelete: () => setSelectedGroupList(selectedGroupList.filter((v) => v._id !== item._id)), - orgs: undefined - })), - ...selectedMemberList.map((item) => ({ - id: `member-${item.tmbId}`, - avatar: item.avatar, - name: item.memberName, - onDelete: () => - setSelectedMemberList(selectedMemberList.filter((v) => v.tmbId !== item.tmbId)), - orgs: item.orgs - })) - ]; - }, [selectedOrgList, selectedGroupList, selectedMemberList, userInfo?.team.teamName]); - return ( { - const Members = members?.map((member) => { - const onChange = () => { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); - } - return [...state, member]; - }); - }; - const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId); - return ( - v.tmbId === member.tmbId)} - orgs={member.orgs} - /> - ); - }); + const MemberList = ( + + ); return searchKey ? ( - Members + MemberList ) : ( - {Members} + {MemberList} ); })()} {(filterClass === 'org' || searchKey) && (() => { const Orgs = orgs?.map((org) => { - const onChange = () => { - setSelectedOrgIdList((state) => { - if (state.find((v) => v._id === org._id)) { - return state.filter((v) => v._id !== org._id); + const addTheOrg = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.orgId === org._id)) { + return state.filter((v) => v.orgId !== org._id); } - return [...state, org]; + return [ + ...state, + { + ...org, + orgId: org._id, + permission: new Permission({ role: defaultRole }) + } + ]; }); }; - const collaborator = collaboratorList?.find((v) => v.orgId === org._id); + const isChecked = !!editCollaborators.find((v) => v.orgId === org._id); return ( String(v._id) === String(org._id))} + onChange={addTheOrg} + isChecked={isChecked} rightSlot={ org.total && ( { onClickOrg(org); - // setPath(getOrgChildrenPath(org)); e.stopPropagation(); }} /> @@ -342,47 +298,33 @@ function MemberModal({ ) : ( {Orgs} - {orgMembers.map((member) => { - const isChecked = !!selectedMemberList.find( - (v) => v.tmbId === member.tmbId - ); - const collaborator = collaboratorList?.find( - (v) => v.tmbId === member.tmbId - ); - return ( - { - setSelectedMemberList((state) => { - if (state.find((v) => v.tmbId === member.tmbId)) { - return state.filter((v) => v.tmbId !== member.tmbId); - } - return [...state, member]; - }); - }} - isChecked={isChecked} - role={collaborator?.permission.role} - addOnly={addOnly && !!member.permission.role} - orgs={member.orgs} - /> - ); - })} + ); })()} {(filterClass === 'group' || searchKey) && groups?.map((group) => { - const onChange = () => { - setSelectedGroupList((state) => { - if (state.find((v) => v._id === group._id)) { - return state.filter((v) => v._id !== group._id); + const addGroup = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.groupId === group._id)) { + return state.filter((v) => v.groupId !== group._id); } - return [...state, group]; + return [ + ...state, + { + ...group, + groupId: group._id, + permission: new Permission({ role: defaultRole }) + } + ]; }); }; - const collaborator = collaboratorList?.find((v) => v.groupId === group._id); + const isChecked = !!editCollaborators.find((v) => v.groupId === group._id); return ( v._id === group._id)} - addOnly={addOnly} + onChange={addGroup} + isChecked={isChecked} /> ); })} @@ -403,18 +343,38 @@ function MemberModal({ {`${t('user:has_chosen')}: `} - {selectedMemberList.length + selectedGroupList.length + selectedOrgList.length} + {editCollaborators.length} - {selectedList.map((item) => { + {editCollaborators.map((clb) => { + const onDelete = () => { + setCollaboratorList((state) => { + return state.filter((v) => v.tmbId !== clb.tmbId); + }); + }; + const onRoleChange = (role: RoleValueType) => { + setCollaboratorList((state) => { + const index = state.findIndex((v) => v.tmbId === clb.tmbId); + if (index === -1) return state; + return [ + ...state.slice(0, index), + { + ...state[index], + permission: new Permission({ role }) + }, + ...state.slice(index + 1) + ]; + }); + }; return ( {}} + onDelete={onDelete} + role={clb.permission.role} + onRoleChange={onRoleChange} /> ); })} @@ -423,32 +383,6 @@ function MemberModal({ - {!addOnly && !!roleList && ( - - {roleLabel} - - - } - onChange={(v) => setSelectedRole(v)} - /> - )} - {addOnly && ( - - - {t('user:permission_add_tip')} - - )} @@ -458,3 +392,51 @@ function MemberModal({ } export default MemberModal; + +const RenderMemberList = ({ + members, + setCollaboratorList, + editCollaborators, + defaultRole +}: { + members: TeamMemberItemType[]; + setCollaboratorList: React.Dispatch>; + editCollaborators: CollaboratorItemDetailType[]; + defaultRole: RoleValueType; +}) => { + return ( + <> + {members?.map((member) => { + const addTheMember = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.tmbId === member.tmbId)) { + return state.filter((v) => v.tmbId !== member.tmbId); + } + return [ + ...state, + { + tmbId: member.tmbId, + avatar: member.avatar, + name: member.memberName, + teamId: member.teamId, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.tmbId === member.tmbId); + return ( + + ); + })} + + ); +}; diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index 6d4e29700c75..b067b8c594c3 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -18,6 +18,8 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; import { CollaboratorContext } from './context'; import { useTranslation } from 'next-i18next'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; export type PermissionSelectProps = { value?: RoleValueType; @@ -47,16 +49,15 @@ function RoleSelect({ offset = [0, 5], Button, width = 'auto', - onDelete + onDelete, + disabled }: PermissionSelectProps) { const { t } = useTranslation(); const ref = useRef(null); const closeTimer = useRef(); - const { permission, roleList: permissionList } = useContextSelector( - CollaboratorContext, - (v) => v - ); + const { roleList: permissionList } = useContextSelector(CollaboratorContext, (v) => v); + const permission = useContextSelector(CollaboratorContext, (v) => v.permission); const [isOpen, setIsOpen] = useState(false); @@ -120,6 +121,7 @@ function RoleSelect({ ref={ref} w="fit-content" onMouseEnter={() => { + if (disabled) return; if (trigger === 'hover') { setIsOpen(true); } @@ -135,8 +137,10 @@ function RoleSelect({ > { if (trigger === 'click') { + if (disabled) return; setIsOpen(!isOpen); } }} diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 18b3aadc5a28..c606b2e6683a 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -1,6 +1,6 @@ import { useDisclosure } from '@chakra-ui/react'; import type { - CollaboratorItemType, + CollaboratorItemDetailType, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; import { Permission } from '@fastgpt/global/support/permission/controller'; @@ -19,14 +19,14 @@ import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { useTranslation } from 'next-i18next'; -import { CommonRoleList } from '@fastgpt/global/support/permission/constant'; +import { CommonRoleList, NullRoleVal } from '@fastgpt/global/support/permission/constant'; const MemberModal = dynamic(() => import('./MemberModal')); -const ManageModal = dynamic(() => import('./ManageModal')); export type MemberManagerInputPropsType = { permission: Permission; - onGetCollaboratorList: () => Promise; + defaultRole: RoleValueType; + onGetCollaboratorList: () => Promise; roleList?: RoleListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; onDelOneCollaborator: ( @@ -36,20 +36,19 @@ export type MemberManagerInputPropsType = { }; export type MemberManagerPropsType = MemberManagerInputPropsType & { - collaboratorList: CollaboratorItemType[]; + collaboratorList: CollaboratorItemDetailType[]; refetchCollaboratorList: () => void; isFetchingCollaborator: boolean; getRoleLabelList: (role: RoleValueType) => string[]; }; + export type ChildrenProps = { - onOpenAddMember: () => void; onOpenManageModal: () => void; MemberListCard: (props: MemberListCardProps) => JSX.Element; }; -type CollaboratorContextType = MemberManagerPropsType & {}; - -export const CollaboratorContext = createContext({ +export const CollaboratorContext = createContext({ + defaultRole: NullRoleVal, collaboratorList: [], roleList: CommonRoleList, onUpdateCollaborators: () => { @@ -64,7 +63,7 @@ export const CollaboratorContext = createContext({ refetchCollaboratorList: (): void => { throw new Error('Function not implemented.'); }, - onGetCollaboratorList: (): Promise => { + onGetCollaboratorList: (): Promise => { throw new Error('Function not implemented.'); }, isFetchingCollaborator: false, @@ -82,7 +81,8 @@ const CollaboratorContextProvider = ({ refreshDeps = [], isInheritPermission, hasParent, - addPermissionOnly + addPermissionOnly, + defaultRole }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; refetchResource?: () => void; @@ -115,6 +115,7 @@ const CollaboratorContextProvider = ({ return data.map((item) => { return { ...item, + // because the permission is not initialized in the frontend, so we need to initialize it here permission: new Permission({ role: item.permission.role }) @@ -161,11 +162,6 @@ const CollaboratorContextProvider = ({ ); const { ConfirmModal, openConfirm } = useConfirm({}); - const { - isOpen: isOpenAddMember, - onOpen: onOpenAddMember, - onClose: onCloseAddMember - } = useDisclosure(); const { isOpen: isOpenManageModal, onOpen: onOpenManageModal, @@ -181,53 +177,18 @@ const CollaboratorContextProvider = ({ roleList, onUpdateCollaborators: onUpdateCollaboratorsThen, onDelOneCollaborator: onDelOneCollaboratorThen, - getRoleLabelList + getRoleLabelList, + defaultRole }; - const onOpenAddMemberModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenAddMember(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenAddMember(); - } - }; - const onOpenManageModalModal = () => { - if (isInheritPermission && hasParent) { - openConfirm( - () => { - onOpenManageModal(); - }, - undefined, - t('common:permission.Remove InheritPermission Confirm') - )(); - } else { - onOpenManageModal(); - } - }; return ( {children({ - onOpenAddMember: onOpenAddMemberModal, - onOpenManageModal: onOpenManageModalModal, + onOpenManageModal, MemberListCard })} - {isOpenAddMember && ( - { - onCloseAddMember(); - refetchResource?.(); - }} - addPermissionOnly={addPermissionOnly} - /> - )} {isOpenManageModal && ( - { onCloseManageModal(); refetchResource?.(); diff --git a/projects/app/src/pageComponents/dashboard/apps/List.tsx b/projects/app/src/pageComponents/dashboard/apps/List.tsx index 43b9cc32351a..8bf672eb966c 100644 --- a/projects/app/src/pageComponents/dashboard/apps/List.tsx +++ b/projects/app/src/pageComponents/dashboard/apps/List.tsx @@ -17,7 +17,7 @@ import { useFolderDrag } from '@/components/common/folder/useFolderDrag'; import dynamic from 'next/dynamic'; import type { EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyMenu, { type MenuItemType } from '@fastgpt/web/components/common/MyMenu'; -import { AppRoleList } from '@fastgpt/global/support/permission/app/constant'; +import { AppDefaultRoleVal, AppRoleList } from '@fastgpt/global/support/permission/app/constant'; import { deleteAppCollaborators, getCollaboratorList, @@ -38,6 +38,7 @@ import { type RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import UserBox from '@fastgpt/web/components/common/UserBox'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { ChatSidebarPaneEnum } from '@/pageComponents/chat/constants'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const HttpEditModal = dynamic(() => import('./HttpPluginEditModal')); const ListItem = () => { @@ -435,15 +436,11 @@ const ListItem = () => { avatar={editPerApp.avatar} name={editPerApp.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerApp.permission, onGetCollaboratorList: () => getCollaboratorList(editPerApp._id), roleList: AppRoleList, - onUpdateCollaborators: (props: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => + onUpdateCollaborators: (props) => postUpdateAppCollaborators({ ...props, appId: editPerApp._id diff --git a/projects/app/src/web/core/app/api/collaborator.ts b/projects/app/src/web/core/app/api/collaborator.ts index fda0564c33a2..97824b167f4c 100644 --- a/projects/app/src/web/core/app/api/collaborator.ts +++ b/projects/app/src/web/core/app/api/collaborator.ts @@ -3,10 +3,13 @@ import type { AppCollaboratorDeleteParams } from '@fastgpt/global/core/app/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (appId: string) => - GET('/proApi/core/app/collaborator/list', { appId }); + GET('/proApi/core/app/collaborator/list', { appId }); export const postUpdateAppCollaborators = (body: UpdateAppCollaboratorBody) => POST('/proApi/core/app/collaborator/update', body); diff --git a/projects/app/src/web/core/dataset/api/collaborator.ts b/projects/app/src/web/core/dataset/api/collaborator.ts index 6c69d73b50c7..91f83eaf9910 100644 --- a/projects/app/src/web/core/dataset/api/collaborator.ts +++ b/projects/app/src/web/core/dataset/api/collaborator.ts @@ -3,10 +3,13 @@ import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (datasetId: string) => - GET('/proApi/core/dataset/collaborator/list', { datasetId }); + GET('/proApi/core/dataset/collaborator/list', { datasetId }); export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => POST('/proApi/core/dataset/collaborator/update', body); diff --git a/projects/app/src/web/support/user/team/group/api.ts b/projects/app/src/web/support/user/team/group/api.ts index 9a2975e8328d..aba4f93feee8 100644 --- a/projects/app/src/web/support/user/team/group/api.ts +++ b/projects/app/src/web/support/user/team/group/api.ts @@ -10,11 +10,7 @@ import type { } from '@fastgpt/global/support/user/team/group/api'; export const getGroupList = (data: GetGroupListBody) => - POST[]>('/proApi/support/user/team/group/list', data).then((res) => { - console.log(res); - return res; - }); - + POST[]>('/proApi/support/user/team/group/list', data); export const postCreateGroup = (data: postCreateGroupData) => POST('/proApi/support/user/team/group/create', data); From 4d0fc39831165424ddec2993aaaf80fd1da5f3d0 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Fri, 22 Aug 2025 15:51:21 +0800 Subject: [PATCH 04/17] feat: support fe permission conflict check --- .../support/permission/collaborator.d.ts | 5 + packages/global/support/permission/utils.ts | 7 +- .../service/support/permission/controller.ts | 107 ++-- .../support/permission/inheritPermission.ts | 131 +++-- .../components/common/folder/SlideCard.tsx | 31 +- .../MemberManager/MemberItemCard.tsx | 2 - .../permission/MemberManager/MemberModal.tsx | 508 ++++++++++-------- .../permission/MemberManager/context.tsx | 46 +- .../src/pageComponents/dataset/list/List.tsx | 2 + .../app/src/pages/dashboard/apps/index.tsx | 2 + .../app/src/web/core/app/api/collaborator.ts | 7 +- 11 files changed, 466 insertions(+), 382 deletions(-) diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 33e325e339cf..b8ecc82f7f68 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -27,3 +27,8 @@ export type UpdateClbPermissionProps = { }; export type DeletePermissionQuery = CollaboratorIdType; + +export type CollaboratorListType = { + clbs: CollaboratorItemDetailType[]; + parentClbs: CollaboratorItemDetailType[]; +}; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index 4fa922f8ac6c..b486e71f90a4 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -41,18 +41,13 @@ export const checkRoleUpdateConflict = ({ } // Use a Map for faster lookup by teamId - const [oldClbRoleMap, parentClbRoleMap] = [ - new Map(oldChildClbs.map((clb) => [getCollaboratorId(clb), clb])), - new Map(parentClbs.map((clb) => [getCollaboratorId(clb), clb])) - ]; + const parentClbRoleMap = new Map(parentClbs.map((clb) => [getCollaboratorId(clb), clb])); const changedClbs = getChangedCollaborators({ newClbs: newChildClbs, oldClbs: oldChildClbs }); - console.log('changedClbs', changedClbs); - console.log('parentClbRoleMap', parentClbRoleMap); for (const changedClb of changedClbs) { const parent = parentClbRoleMap.get(getCollaboratorId(changedClb)); if (parent && (changedClb.changedRole & parent.permission) !== 0) { diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index e002ce806664..1492080b8a74 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -22,10 +22,7 @@ import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; import { authUserSession } from '../user/session'; import { sumPer } from '@fastgpt/global/support/permission/utils'; -import { - CollaboratorItemDetailType, - SimpleCollaboratorItemType -} from '@fastgpt/global/support/permission/collaborator'; +import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -135,9 +132,11 @@ export async function getResourceClbs({ export const getClbsWithInfo = async ({ resourceId, resourceType, - teamId + teamId, + tmbId }: { teamId: string; + tmbId?: string; } & ( | { resourceId: ParentIdType; @@ -147,42 +146,72 @@ export const getClbsWithInfo = async ({ resourceType: 'team'; resourceId?: undefined; } -)) => - Promise.all([ - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - tmbId: { - $exists: true - } - }) - .populate<{ tmb: TeamMemberSchema }>({ - path: 'tmb', - select: 'name userId avatar' +)) => { + if (!resourceId) { + return []; + } + return Promise.all([ + ...( + await MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + tmbId: { + $exists: true + } }) - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - groupId: { - $exists: true - } - }) - .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') - .lean(), - MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - orgId: { - $exists: true - } - }) - .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) - .lean() + .populate<{ tmb: TeamMemberSchema }>({ + path: 'tmb', + select: 'name userId avatar' + }) + .lean() + ) + .map((item) => ({ + tmbId: item.tmb._id, + teamId: item.teamId, + permission: new Permission({ role: item.permission, isOwner: item.tmbId === tmbId }), + name: item.tmb.name, + avatar: item.tmb.avatar + })) + .sort((a, b) => a.name.localeCompare(b.name)), + ...( + await MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + groupId: { + $exists: true + } + }) + .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') + .lean() + ).map((item) => ({ + groupId: item.group._id, + teamId: item.teamId, + permission: new Permission({ role: item.permission }), + name: item.group.name, + avatar: item.group.avatar + })), + ...( + await MongoResourcePermission.find({ + teamId, + resourceId, + resourceType, + orgId: { + $exists: true + } + }) + .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) + .lean() + ).map((item) => ({ + orgId: item.org._id, + teamId: item.teamId, + permission: new Permission({ role: item.permission }), + name: item.org.name, + avatar: item.org.avatar || DEFAULT_ORG_AVATAR + })) ]); +}; export const delResourcePermissionById = (id: string) => { return MongoResourcePermission.findByIdAndDelete(id); diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index fdf7e2529963..3e57c5c1a867 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -127,9 +127,7 @@ export async function syncChildrenPermission({ for (const child of _children) { // 1. get parent's permission and what permission I have. const parentClbs = resourceIdClbMap.get(String(child.parentId)) || []; - console.log('parentClbs', parentClbs); const myClbs = resourceIdClbMap.get(String(child._id)) || []; - console.log('myClbs', myClbs); // 2. merge the permission and generate operations for mongo. // rules: // i. if parent has and I have not, get the clb. @@ -158,10 +156,6 @@ export async function syncChildrenPermission({ !parentClbs.some((parentClb) => getCollaboratorId(clb) === getCollaboratorId(parentClb)) ); - console.log('bothHaveClbs', bothHaveClbs); - console.log('parentHasAndIHaveNot', parentHasAndIHaveNot); - console.log('IHaveAndParentHasNot', IHaveAndParentHasNot); - // generate ops // i. for (const clb of parentHasAndIHaveNot) { @@ -226,7 +220,6 @@ export async function syncChildrenPermission({ queue.push(child._id); } } - console.log(ops, JSON.stringify(ops, null, 2)); await MongoResourcePermission.bulkWrite(ops, { session }); return; } @@ -263,37 +256,32 @@ export async function resumeInheritPermission({ ); // Folder resource, need to sync children - if (isFolder) { - const parentClbsAndGroups = await getResourceClbs({ - resourceId: resource.parentId, - teamId: resource.teamId, - resourceType, - session - }); + const parentClbs = await getResourceClbs({ + resourceId: resource.parentId, + teamId: resource.teamId, + resourceType, + session + }); - // sync self - await syncCollaborators({ - resourceType, - collaborators: parentClbsAndGroups, - teamId: resource.teamId, - resourceId: resource._id, - session - }); - // sync children - await syncChildrenPermission({ - resource: { - ...resource - }, - resourceModel, - folderTypeList, - resourceType, - session, - collaborators: parentClbsAndGroups - }); - } else { - // Not folder, delete all clb - await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session }); - } + // sync self + await syncCollaborators({ + resourceType, + collaborators: parentClbs, + teamId: resource.teamId, + resourceId: resource._id, + session + }); + // sync children + await syncChildrenPermission({ + resource, + resourceModel, + folderTypeList, + resourceType, + session, + collaborators: parentClbs + }); + // Not folder, delete all clb + // await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session }); }; if (session) { @@ -319,27 +307,52 @@ export async function syncCollaborators({ collaborators: CollaboratorItemType[]; session: ClientSession; }) { - await MongoResourcePermission.deleteMany( - { - resourceType, - teamId, - resourceId - }, - { session } - ); - await MongoResourcePermission.insertMany( - collaborators.map((item) => ({ - teamId: teamId, - resourceId, - resourceType: resourceType, - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId, - permission: item.permission - })), - { - session, - ordered: true - } + const parentClbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), clb])); + const clbsNow = await MongoResourcePermission.find({ + resourceType, + teamId, + resourceId + }) + .lean() + .session(session); + const ops: AnyBulkWriteOperation[] = []; + for (const clb of clbsNow) { + const parentClb = parentClbMap.get(getCollaboratorId(clb)); + const permission = sumPer(parentClb?.permission ?? NullPermissionVal, clb.permission); + ops.push({ + updateOne: { + filter: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb) + }, + update: { + permission, + selfPermission: clb.permission + } + } + }); + } + + const parentHasAndIHaveNot = collaborators.filter( + (clb) => !clbsNow.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) ); + + for (const clb of parentHasAndIHaveNot) { + ops.push({ + insertOne: { + document: { + teamId, + resourceId, + resourceType, + ...pickCollaboratorIdFields(clb), + permission: clb.permission, + selfPermission: NullPermissionVal + } as ResourcePermissionType + } + }); + } + + await MongoResourcePermission.bulkWrite(ops, { session }); } diff --git a/projects/app/src/components/common/folder/SlideCard.tsx b/projects/app/src/components/common/folder/SlideCard.tsx index e4908f1dc74d..21eb39850374 100644 --- a/projects/app/src/components/common/folder/SlideCard.tsx +++ b/projects/app/src/components/common/folder/SlideCard.tsx @@ -137,7 +137,7 @@ const FolderSlideCard = ({ isInheritPermission={isInheritPermission} hasParent={hasParent} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> @@ -145,26 +145,15 @@ const FolderSlideCard = ({ {t('common:permission.Collaborator')} {managePer.permission.hasManagePer && ( - - - - - - - - + + + )} void }) { (v) => v.onUpdateCollaborators ); - const { runAsync: onConfirm, loading: isUpdating } = useRequest2( + const parentClbs = useContextSelector(CollaboratorContext, (v) => v.parentClbList); + + const { runAsync: _onConfirm, loading: isUpdating } = useRequest2( () => onUpdateCollaborators({ collaborators: editCollaborators.map( @@ -121,6 +127,40 @@ function MemberModal({ onClose }: { onClose: () => void }) { } ); + // TODO: I18n + const { openConfirm: openConfirmDisableInheritPer, ConfirmModal: ConfirmDisableInheritPer } = + useConfirm({ + content: t('permission.Remove InheritPermission Confirm') + }); + + const onConfirm = useCallback(() => { + const isConflict = checkRoleUpdateConflict({ + parentClbs: parentClbs.map((clb) => ({ + ...clb, + permission: clb.permission.role + })), + oldChildClbs: collaboratorDetailList.map((clb) => ({ + ...clb, + permission: clb.permission.role + })), + newChildClbs: editCollaborators.map((clb) => ({ + ...clb, + permission: clb.permission.role + })) + }); + if (!isConflict) { + return _onConfirm(); + } else { + openConfirmDisableInheritPer(_onConfirm)(); + } + }, [ + _onConfirm, + collaboratorDetailList, + editCollaborators, + openConfirmDisableInheritPer, + parentClbs + ]); + const entryList = useRef([ { label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' }, { label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' }, @@ -128,266 +168,272 @@ function MemberModal({ onClose }: { onClose: () => void }) { ]); return ( - - - - + + + - setSearchKey(e.target.value)} - /> + + setSearchKey(e.target.value)} + /> - - {/* Entry */} - {!searchKey && !filterClass && ( - <> - {entryList.current.map((item) => { - return ( - setFilterClass(item.value as any)} - > - - - {item.label} - - - - ); - })} - - )} + + {/* Entry */} + {!searchKey && !filterClass && ( + <> + {entryList.current.map((item) => { + return ( + setFilterClass(item.value as any)} + > + + + {item.label} + + + + ); + })} + + )} - {/* Path */} - {!searchKey && filterClass && ( - - { - if (parentId === '') { - setFilterClass(undefined); - onPathClick(''); - } else if ( - parentId === 'member' || - parentId === 'org' || - parentId === 'group' - ) { - setFilterClass(parentId); - onPathClick(''); - } else { - onPathClick(parentId); - } - }} - rootName={t('common:Team')} - /> - - )} - {(filterClass === 'member' || searchKey) && - (() => { - const MemberList = ( - + { + if (parentId === '') { + setFilterClass(undefined); + onPathClick(''); + } else if ( + parentId === 'member' || + parentId === 'org' || + parentId === 'group' + ) { + setFilterClass(parentId); + onPathClick(''); + } else { + onPathClick(parentId); + } + }} + rootName={t('common:Team')} /> - ); - return searchKey ? ( - MemberList - ) : ( - - {MemberList} - - ); - })()} - {(filterClass === 'org' || searchKey) && - (() => { - const Orgs = orgs?.map((org) => { - const addTheOrg = () => { + + )} + {(filterClass === 'member' || searchKey) && + (() => { + const MemberList = ( + + ); + return searchKey ? ( + MemberList + ) : ( + + {MemberList} + + ); + })()} + {(filterClass === 'org' || searchKey) && + (() => { + const Orgs = orgs?.map((org) => { + const addTheOrg = () => { + setCollaboratorList((state) => { + if (state.find((v) => v.orgId === org._id)) { + return state.filter((v) => v.orgId !== org._id); + } + return [ + ...state, + { + ...org, + orgId: org._id, + permission: new Permission({ role: defaultRole }) + } + ]; + }); + }; + const isChecked = !!editCollaborators.find((v) => v.orgId === org._id); + return ( + { + onClickOrg(org); + e.stopPropagation(); + }} + /> + ) + } + /> + ); + }); + return searchKey ? ( + Orgs + ) : ( + + {Orgs} + + + ); + })()} + {(filterClass === 'group' || searchKey) && + groups?.map((group) => { + const addGroup = () => { setCollaboratorList((state) => { - if (state.find((v) => v.orgId === org._id)) { - return state.filter((v) => v.orgId !== org._id); + if (state.find((v) => v.groupId === group._id)) { + return state.filter((v) => v.groupId !== group._id); } return [ ...state, { - ...org, - orgId: org._id, + ...group, + groupId: group._id, permission: new Permission({ role: defaultRole }) } ]; }); }; - const isChecked = !!editCollaborators.find((v) => v.orgId === org._id); + const isChecked = !!editCollaborators.find((v) => v.groupId === group._id); return ( { - onClickOrg(org); - e.stopPropagation(); - }} - /> - ) + avatar={group.avatar} + key={group._id} + name={ + group.name === DefaultGroupName + ? userInfo?.team.teamName ?? '' + : group.name } + onChange={addGroup} + isChecked={isChecked} /> ); - }); - return searchKey ? ( - Orgs - ) : ( - - {Orgs} - - - ); - })()} - {(filterClass === 'group' || searchKey) && - groups?.map((group) => { - const addGroup = () => { + })} + + + + + + {`${t('user:has_chosen')}: `} + {editCollaborators.length} + + + {editCollaborators.map((clb) => { + const onDelete = () => { setCollaboratorList((state) => { - if (state.find((v) => v.groupId === group._id)) { - return state.filter((v) => v.groupId !== group._id); - } + return state.filter((v) => getCollaboratorId(v) !== getCollaboratorId(clb)); + }); + }; + const onRoleChange = (role: RoleValueType) => { + setCollaboratorList((state) => { + const index = state.findIndex( + (v) => getCollaboratorId(v) === getCollaboratorId(clb) + ); + if (index === -1) return state; return [ - ...state, + ...state.slice(0, index), { - ...group, - groupId: group._id, - permission: new Permission({ role: defaultRole }) - } + ...state[index], + permission: new Permission({ role }) + }, + ...state.slice(index + 1) ]; }); }; - const isChecked = !!editCollaborators.find((v) => v.groupId === group._id); return ( ); })} + - - - - - {`${t('user:has_chosen')}: `} - {editCollaborators.length} - - - {editCollaborators.map((clb) => { - const onDelete = () => { - setCollaboratorList((state) => { - return state.filter((v) => v.tmbId !== clb.tmbId); - }); - }; - const onRoleChange = (role: RoleValueType) => { - setCollaboratorList((state) => { - const index = state.findIndex((v) => v.tmbId === clb.tmbId); - if (index === -1) return state; - return [ - ...state.slice(0, index), - { - ...state[index], - permission: new Permission({ role }) - }, - ...state.slice(index + 1) - ]; - }); - }; - return ( - {}} - onDelete={onDelete} - role={clb.permission.role} - onRoleChange={onRoleChange} - /> - ); - })} - - - - - - - - + + + + + + + + ); } diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index c606b2e6683a..5510dc4a4291 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -1,6 +1,7 @@ import { useDisclosure } from '@chakra-ui/react'; import type { CollaboratorItemDetailType, + CollaboratorListType, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; import { Permission } from '@fastgpt/global/support/permission/controller'; @@ -26,7 +27,7 @@ const MemberModal = dynamic(() => import('./MemberModal')); export type MemberManagerInputPropsType = { permission: Permission; defaultRole: RoleValueType; - onGetCollaboratorList: () => Promise; + onGetCollaboratorList: () => Promise; roleList?: RoleListType; onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise; onDelOneCollaborator: ( @@ -35,8 +36,9 @@ export type MemberManagerInputPropsType = { refreshDeps?: any[]; }; -export type MemberManagerPropsType = MemberManagerInputPropsType & { +export type CollaboratorContextType = MemberManagerInputPropsType & { collaboratorList: CollaboratorItemDetailType[]; + parentClbList: CollaboratorItemDetailType[]; refetchCollaboratorList: () => void; isFetchingCollaborator: boolean; getRoleLabelList: (role: RoleValueType) => string[]; @@ -47,9 +49,10 @@ export type ChildrenProps = { MemberListCard: (props: MemberListCardProps) => JSX.Element; }; -export const CollaboratorContext = createContext({ +export const CollaboratorContext = createContext({ defaultRole: NullRoleVal, collaboratorList: [], + parentClbList: [], roleList: CommonRoleList, onUpdateCollaborators: () => { throw new Error('Function not implemented.'); @@ -79,9 +82,6 @@ const CollaboratorContextProvider = ({ children, refetchResource, refreshDeps = [], - isInheritPermission, - hasParent, - addPermissionOnly, defaultRole }: MemberManagerInputPropsType & { children: (props: ChildrenProps) => ReactNode; @@ -105,24 +105,31 @@ const CollaboratorContextProvider = ({ const { feConfigs } = useSystemStore(); const { - data: collaboratorList = [], + data: { clbs: collaboratorList = [], parentClbs: parentClbList = [] } = { + clbs: [], + parentClbs: [] + }, runAsync: refetchCollaboratorList, loading: isFetchingCollaborator } = useRequest2( async () => { if (feConfigs.isPlus) { - const data = await onGetCollaboratorList(); - return data.map((item) => { - return { - ...item, - // because the permission is not initialized in the frontend, so we need to initialize it here - permission: new Permission({ - role: item.permission.role - }) - }; - }); + const { clbs, parentClbs } = await onGetCollaboratorList(); + return { + clbs: clbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })), + parentClbs: parentClbs.map((clb) => ({ + ...clb, + permission: new Permission({ role: clb.permission.role }) + })) + }; } - return []; + return { + clbs: [], + parentClbs: [] + }; }, { manual: false, @@ -178,7 +185,8 @@ const CollaboratorContextProvider = ({ onUpdateCollaborators: onUpdateCollaboratorsThen, onDelOneCollaborator: onDelOneCollaboratorThen, getRoleLabelList, - defaultRole + defaultRole, + parentClbList }; return ( diff --git a/projects/app/src/pageComponents/dataset/list/List.tsx b/projects/app/src/pageComponents/dataset/list/List.tsx index eaac380819d3..43e4282d8687 100644 --- a/projects/app/src/pageComponents/dataset/list/List.tsx +++ b/projects/app/src/pageComponents/dataset/list/List.tsx @@ -32,6 +32,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; import SideTag from './SideTag'; import { getModelProvider } from '@fastgpt/global/core/ai/provider'; import UserBox from '@fastgpt/web/components/common/UserBox'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); @@ -434,6 +435,7 @@ function List() { avatar={editPerDataset.avatar} name={editPerDataset.name} managePer={{ + defaultRole: ReadRoleVal, permission: editPerDataset.permission, onGetCollaboratorList: () => getCollaboratorList(editPerDataset._id), roleList: DatasetRoleList, diff --git a/projects/app/src/pages/dashboard/apps/index.tsx b/projects/app/src/pages/dashboard/apps/index.tsx index 3079ed7df9fc..c6ebbcd620e4 100644 --- a/projects/app/src/pages/dashboard/apps/index.tsx +++ b/projects/app/src/pages/dashboard/apps/index.tsx @@ -34,6 +34,7 @@ import MCPToolsEditModal from '@/pageComponents/dashboard/apps/MCPToolsEditModal import { getUtmWorkflow } from '@/web/support/marketing/utils'; import { useMount } from 'ahooks'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const CreateModal = dynamic(() => import('@/pageComponents/dashboard/apps/CreateModal')); const EditFolderModal = dynamic( @@ -282,6 +283,7 @@ const MyApps = ({ MenuIcon }: { MenuIcon: JSX.Element }) => { deleteTip={t('app:confirm_delete_folder_tip')} onDelete={() => onDeleFolder(folderDetail._id)} managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: AppRoleList, diff --git a/projects/app/src/web/core/app/api/collaborator.ts b/projects/app/src/web/core/app/api/collaborator.ts index 97824b167f4c..27b6ff33cfe3 100644 --- a/projects/app/src/web/core/app/api/collaborator.ts +++ b/projects/app/src/web/core/app/api/collaborator.ts @@ -3,13 +3,10 @@ import type { AppCollaboratorDeleteParams } from '@fastgpt/global/core/app/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { - CollaboratorItemDetailType, - CollaboratorItemType -} from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (appId: string) => - GET('/proApi/core/app/collaborator/list', { appId }); + GET('/proApi/core/app/collaborator/list', { appId }); export const postUpdateAppCollaborators = (body: UpdateAppCollaboratorBody) => POST('/proApi/core/app/collaborator/update', body); From bfd8d3fc00f589dadb7d3b974d29edb5b11486cb Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 01:17:05 +0800 Subject: [PATCH 05/17] refactor(permission): app permission --- packages/global/support/permission/utils.ts | 44 +++--- .../service/support/permission/controller.ts | 126 +++++++++++++++++- .../support/permission/inheritPermission.ts | 17 ++- .../MemberManager/MemberItemCard.tsx | 15 ++- .../permission/MemberManager/MemberModal.tsx | 23 +++- .../permission/MemberManager/context.tsx | 19 ++- .../pageComponents/dataset/MemberManager.tsx | 2 +- projects/app/src/pages/api/core/app/create.ts | 19 ++- .../src/pages/api/core/app/folder/create.ts | 48 ++----- projects/app/src/pages/api/core/app/update.ts | 58 ++++---- projects/app/src/service/core/app/utils.ts | 5 + 11 files changed, 252 insertions(+), 124 deletions(-) diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index b486e71f90a4..93adf75c1346 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,9 +1,4 @@ -import { getClientBuildManifest } from 'next/dist/client/route-loader'; -import type { - CollaboratorIdType, - CollaboratorItemDetailType, - CollaboratorItemType -} from './collaborator'; +import type { CollaboratorIdType, CollaboratorItemType } from './collaborator'; import { type PermissionValueType } from './type'; /** * Sum the permission value. @@ -29,13 +24,14 @@ export const sumPer = (...per: PermissionValueType[]) => { */ export const checkRoleUpdateConflict = ({ parentClbs, - oldChildClbs, + oldRealClbs, newChildClbs }: { parentClbs: CollaboratorItemType[]; - oldChildClbs: CollaboratorItemType[]; + oldRealClbs: CollaboratorItemType[]; newChildClbs: CollaboratorItemType[]; }): boolean => { + console.log('checkRoleUpdateConflict', parentClbs, oldRealClbs, newChildClbs); if (parentClbs.length === 0) { return false; } @@ -44,8 +40,8 @@ export const checkRoleUpdateConflict = ({ const parentClbRoleMap = new Map(parentClbs.map((clb) => [getCollaboratorId(clb), clb])); const changedClbs = getChangedCollaborators({ - newClbs: newChildClbs, - oldClbs: oldChildClbs + newRealClbs: newChildClbs, + oldRealClbs: oldRealClbs }); for (const changedClb of changedClbs) { @@ -65,24 +61,25 @@ export const checkRoleUpdateConflict = ({ * id: string; // collaborator id * changedRole: number; // set bit means the role is changed * } + * special: for low 3 bit: always get the lowest change, unset the higher change. * @param param0 */ export const getChangedCollaborators = ({ - oldClbs, - newClbs + oldRealClbs, + newRealClbs }: { - oldClbs: CollaboratorItemType[]; - newClbs: CollaboratorItemType[]; + oldRealClbs: CollaboratorItemType[]; + newRealClbs: CollaboratorItemType[]; }) => { - if (oldClbs.length === 0) { - return newClbs.map((clb) => ({ + if (oldRealClbs.length === 0) { + return newRealClbs.map((clb) => ({ ...clb, changedRole: clb.permission })); } - const oldClbsMap = new Map(oldClbs.map((clb) => [getCollaboratorId(clb), clb])); + const oldClbsMap = new Map(oldRealClbs.map((clb) => [getCollaboratorId(clb), clb])); const changedClbs = []; - for (const newClb of newClbs) { + for (const newClb of newRealClbs) { const oldClb = oldClbsMap.get(getCollaboratorId(newClb)); if (!oldClb) { changedClbs.push({ @@ -100,8 +97,8 @@ export const getChangedCollaborators = ({ } } - for (const oldClb of oldClbs) { - const newClb = newClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb)); + for (const oldClb of oldRealClbs) { + const newClb = newRealClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(oldClb)); if (!newClb) { changedClbs.push({ ...oldClb, @@ -111,6 +108,13 @@ export const getChangedCollaborators = ({ } } + changedClbs.forEach((clb) => { + // For the lowest 3 bits, only keep the lowest set bit as 1, clear other lower bits, keep higher bits unchanged + const low3 = clb.changedRole & 0b111; + const lowestBit = low3 & -low3; + clb.changedRole = (clb.changedRole & ~0b111) | lowestBit; + }); + return changedClbs; }; diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 1492080b8a74..b5184633272a 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,15 +1,22 @@ import Cookie from 'cookie'; +import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import jwt from 'jsonwebtoken'; import { type NextApiResponse, type NextApiRequest } from 'next'; import type { AuthModeType, ReqHeaderAuthType } from './type.d'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { + AuthUserTypeEnum, + ManagePermissionVal, + ManageRoleVal, + NullRoleVal, + OwnerRoleVal +} from '@fastgpt/global/support/permission/constant'; import { authOpenApiKey } from '../openapi/auth'; import { type FileTokenQuery } from '@fastgpt/global/common/file/type'; import { MongoResourcePermission } from './schema'; -import { type ClientSession } from 'mongoose'; -import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; +import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; +import { ResourceType, type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { addMinutes } from 'date-fns'; import { getGroupsByTmbId } from './memberGroup/controllers'; @@ -21,8 +28,12 @@ import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; import { authUserSession } from '../user/session'; -import { sumPer } from '@fastgpt/global/support/permission/utils'; +import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; +import { syncCollaborators, type SyncChildrenPermissionResourceType } from './inheritPermission'; +import { pickCollaboratorIdFields } from './utils'; +import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -436,3 +447,110 @@ export const authFileToken = (token?: string) => }); }); }); + +export const createResourceDefaultCollaborators = async ({ + resource, + folderTypeList, + resourceType, + resourceModel, + session, + tmbId +}: { + resource: SyncChildrenPermissionResourceType; + + // when the resource is a folder + folderTypeList: string[]; + + resourceModel: typeof Model; + resourceType: PerResourceTypeEnum; + + // should be provided when inheritPermission is true + session: ClientSession; + tmbId: string; +}) => { + console.log('resource', resource); + const parentClbs = await getResourceClbs({ + resourceId: resource.parentId, + resourceType, + teamId: resource.teamId, + session + }); + // 1. add owner into the permission list with owner per + // 2. remove parent's owner permission, instead of manager + + const collaborators: CollaboratorItemType[] = [ + ...parentClbs + .filter((item) => item.tmbId !== tmbId) + .map((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; + clb.selfPermission = NullRoleVal; + } + return clb; + }), + { + tmbId, + permission: OwnerRoleVal + } + ]; + + const ops: AnyBulkWriteOperation[] = []; + + for (const clb of collaborators) { + ops.push({ + insertOne: { + document: { + ...pickCollaboratorIdFields(clb), + teamId: resource.teamId, + resourceId: resource._id, + permission: clb.permission, + selfPermission: NullRoleVal, + resourceType + } as ResourcePermissionType + } + }); + } + + // for (const parentClb of parentClbs) { + // ops.push({ + // insertOne: { + // document: { + // ...pickCollaboratorIdFields(parentClb), + // teamId: resource.teamId, + // resourceId: resource._id, + // permission: parentClb.permission === OwnerRoleVal ? ManageRoleVal : parentClb.permission, + // selfPermission: NullRoleVal, + // resourceType + // } as ResourcePermissionType + // } + // }); + // } + + // ops.push({ + // updateOne: { + // filter: { + // resourceId: resource._id, + // teamId: resource.teamId, + // resourceType, + // tmbId + // }, + // update: { + // permission: OwnerRoleVal, + // selfPermission: OwnerRoleVal + // }, + // upsert: true + // } + // }); + + await MongoResourcePermission.bulkWrite(ops, { session }); + + // if (resource.type in folderTypeList) { + // await syncCollaborators({ + // collaborators, + // resourceId: resource._id, + // resourceType, + // session, + // teamId: resource.teamId + // }); + // } +}; diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index 3e57c5c1a867..a7345e0e8aa3 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -1,7 +1,8 @@ import type { ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { + ManageRoleVal, NullPermissionVal, - NullRoleVal, + OwnerRoleVal, type PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; @@ -9,7 +10,6 @@ import { mongoSessionRun } from '../../common/mongo/sessionRun'; import { getResourceClbs } from './controller'; import { MongoResourcePermission } from './schema'; import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; -import { Types } from '../../common/mongo'; import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; import { pickCollaboratorIdFields } from './utils'; @@ -46,7 +46,6 @@ export async function syncChildrenPermission({ collaborators?: CollaboratorItemType[]; }) { - console.log('collaborators', collaborators); // only folder has permission const isFolder = folderTypeList.includes(resource.type); const teamId = resource.teamId; @@ -241,8 +240,6 @@ export async function resumeInheritPermission({ resourceModel: typeof Model; session?: ClientSession; }) { - const isFolder = folderTypeList.includes(resource.type); - const fn = async (session: ClientSession) => { // update the resource permission await resourceModel.updateOne( @@ -292,8 +289,8 @@ export async function resumeInheritPermission({ } /** - Delete all the collaborators and then insert the new collaborators. -*/ + * sync parent collaborators to children. + */ export async function syncCollaborators({ resourceType, teamId, @@ -307,6 +304,12 @@ export async function syncCollaborators({ collaborators: CollaboratorItemType[]; session: ClientSession; }) { + // should change parent owner permission into manage + collaborators.forEach((clb) => { + if (clb.permission === OwnerRoleVal) { + clb.permission = ManageRoleVal; + } + }); const parentClbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), clb])); const clbsNow = await MongoResourcePermission.find({ resourceType, diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index adf1c71858a1..e61201ce2a03 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -19,7 +19,8 @@ function MemberItemCard({ role, orgs, rightSlot, - onRoleChange + onRoleChange, + disabled = false }: { avatar: string; key: string; @@ -31,8 +32,8 @@ function MemberItemCard({ role?: RoleValueType; orgs?: string[]; rightSlot?: React.ReactNode; + disabled?: boolean; }) { - const isDisabled = role === OwnerRoleVal; return ( { - if (isDisabled) return; + if (disabled) return; onChange?.(); }} > @@ -62,7 +63,7 @@ function MemberItemCard({ {role !== undefined && !!onRoleChange && ( { - if (isDisabled) return; + if (disabled) return; onDelete?.(); }} /> diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 3bc5c8d0ff11..3bf4c3d8496c 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -9,10 +9,8 @@ import { DEFAULT_TEAM_AVATAR, DEFAULT_USER_AVATAR } from '@fastgpt/global/common/system/constants'; -import { type MemberGroupListItemType } from '@fastgpt/global/support/permission/memberGroup/type'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { type OrgListItemType } from '@fastgpt/global/support/user/team/org/type'; -import { TeamTmbItemType, type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; +import { type TeamMemberItemType } from '@fastgpt/global/support/user/team/type'; import MyAvatar from '@fastgpt/web/components/common/Avatar'; import MyIcon from '@fastgpt/web/components/common/Icon'; import SearchInput from '@fastgpt/web/components/common/Input/SearchInput'; @@ -20,7 +18,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useTranslation } from 'next-i18next'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useContextSelector } from 'use-context-selector'; import { CollaboratorContext } from './context'; import MemberItemCard from './MemberItemCard'; @@ -35,6 +33,7 @@ import { getCollaboratorId } from '@fastgpt/global/support/permission/utils'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; +import { OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; const HoverBoxStyle = { bgColor: 'myGray.50', @@ -105,6 +104,7 @@ function MemberModal({ onClose }: { onClose: () => void }) { ); const parentClbs = useContextSelector(CollaboratorContext, (v) => v.parentClbList); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); const { runAsync: _onConfirm, loading: isUpdating } = useRequest2( () => @@ -139,7 +139,7 @@ function MemberModal({ onClose }: { onClose: () => void }) { ...clb, permission: clb.permission.role })), - oldChildClbs: collaboratorDetailList.map((clb) => ({ + oldRealClbs: collaboratorDetailList.map((clb) => ({ ...clb, permission: clb.permission.role })), @@ -172,8 +172,8 @@ function MemberModal({ onClose }: { onClose: () => void }) { void }) { onDelete={onDelete} role={clb.permission.role} onRoleChange={onRoleChange} + disabled={ + clb.permission.role === OwnerRoleVal || + clb.tmbId === userInfo?.team.tmbId || + (clb.permission.hasManagePer && !myRole.isOwner) + } /> ); })} @@ -450,6 +455,7 @@ const RenderMemberList = ({ editCollaborators: CollaboratorItemDetailType[]; defaultRole: RoleValueType; }) => { + const { userInfo } = useUserStore(); return ( <> {members?.map((member) => { @@ -480,6 +486,9 @@ const RenderMemberList = ({ onChange={addTheMember} isChecked={isChecked} orgs={member.orgs} + disabled={ + member.permission.role === OwnerRoleVal || member.tmbId === userInfo?.team.tmbId + } /> ); })} diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index 5510dc4a4291..e3094130ec29 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -10,7 +10,7 @@ import type { RoleListType, RoleValueType } from '@fastgpt/global/support/permission/type'; -import { type ReactNode, useCallback } from 'react'; +import { type ReactNode, useCallback, useMemo } from 'react'; import { createContext } from 'use-context-selector'; import dynamic from 'next/dynamic'; @@ -21,6 +21,7 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; import { useTranslation } from 'next-i18next'; import { CommonRoleList, NullRoleVal } from '@fastgpt/global/support/permission/constant'; +import { useUserStore } from '@/web/support/user/useUserStore'; const MemberModal = dynamic(() => import('./MemberModal')); @@ -39,6 +40,7 @@ export type MemberManagerInputPropsType = { export type CollaboratorContextType = MemberManagerInputPropsType & { collaboratorList: CollaboratorItemDetailType[]; parentClbList: CollaboratorItemDetailType[]; + myRole: Permission; refetchCollaboratorList: () => void; isFetchingCollaborator: boolean; getRoleLabelList: (role: RoleValueType) => string[]; @@ -66,7 +68,7 @@ export const CollaboratorContext = createContext({ refetchCollaboratorList: (): void => { throw new Error('Function not implemented.'); }, - onGetCollaboratorList: (): Promise => { + onGetCollaboratorList: (): Promise => { throw new Error('Function not implemented.'); }, isFetchingCollaborator: false, @@ -168,13 +170,20 @@ const CollaboratorContextProvider = ({ [roleList] ); - const { ConfirmModal, openConfirm } = useConfirm({}); const { isOpen: isOpenManageModal, onOpen: onOpenManageModal, onClose: onCloseManageModal } = useDisclosure(); + const { userInfo } = useUserStore(); + const myRole = useMemo(() => { + return ( + collaboratorList.find((v) => v.tmbId === userInfo?.team?.tmbId)?.permission ?? + new Permission() + ); + }, [collaboratorList, userInfo?.team?.tmbId]); + const contextValue = { permission, onGetCollaboratorList, @@ -186,7 +195,8 @@ const CollaboratorContextProvider = ({ onDelOneCollaborator: onDelOneCollaboratorThen, getRoleLabelList, defaultRole, - parentClbList + parentClbList, + myRole }; return ( @@ -203,7 +213,6 @@ const CollaboratorContextProvider = ({ }} /> )} - ); }; diff --git a/projects/app/src/pageComponents/dataset/MemberManager.tsx b/projects/app/src/pageComponents/dataset/MemberManager.tsx index e2e367bfe4e5..d551f9bb984e 100644 --- a/projects/app/src/pageComponents/dataset/MemberManager.tsx +++ b/projects/app/src/pageComponents/dataset/MemberManager.tsx @@ -11,7 +11,7 @@ function MemberManager({ managePer }: { managePer: MemberManagerInputPropsType } return ( - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index bcfdb9f58588..20a2915b7996 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -6,7 +6,10 @@ import type { AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; import type { AppSchema } from '@fastgpt/global/core/app/type'; import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -22,6 +25,7 @@ import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nAppType } from '@fastgpt/service/support/user/audit/util'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; export type CreateAppBody = { parentId?: ParentIdType; @@ -113,7 +117,7 @@ export const onCreateApp = async ({ session?: ClientSession; }) => { const create = async (session: ClientSession) => { - const [{ _id: appId }] = await MongoApp.create( + const [app] = await MongoApp.create( [ { ...parseParentIdInMongo(parentId), @@ -133,6 +137,8 @@ export const onCreateApp = async ({ { session, ordered: true } ); + const appId = app._id; + if (!AppFolderTypeList.includes(type!)) { await MongoAppVersion.create( [ @@ -151,6 +157,15 @@ export const onCreateApp = async ({ { session, ordered: true } ); } + + await createResourceDefaultCollaborators({ + folderTypeList: AppFolderTypeList, + tmbId, + session, + resource: app, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app + }); (async () => { addAuditLog({ tmbId, 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 88ff0d7e9b1c..2ef4f9a67cf3 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -3,9 +3,8 @@ import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; -import { AppTypeEnum } from '@fastgpt/global/core/app/constants'; +import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -13,9 +12,7 @@ import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/u import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authApp } from '@fastgpt/service/support/permission/app/auth'; -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 { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -50,39 +47,14 @@ async function handler(req: ApiRequestProps) { type: AppTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - collaborators: parentClbsAndGroups, - session - }); - } else { - // Create default permission - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.app, - teamId, - resourceId: app._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { - session, - ordered: true - } - ); - } + await createResourceDefaultCollaborators({ + folderTypeList: AppFolderTypeList, + tmbId, + session, + resource: app, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index a58f8f86f3ce..02a67af0875c 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -18,7 +18,7 @@ import { import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ClientSession } from 'mongoose'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; @@ -150,38 +150,30 @@ async function handler(req: ApiRequestProps) { if (isMove) { await mongoSessionRun(async (session) => { // Inherit folder: Sync children permission and it's clbs - if (AppFolderTypeList.includes(app.type)) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: app.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.app, - session - }); - // sync self - await syncCollaborators({ - resourceId: app._id, - resourceType: PerResourceTypeEnum.app, - collaborators: parentClbsAndGroups, - session, - teamId: app.teamId - }); - // sync the children - await syncChildrenPermission({ - resource: app, - resourceType: PerResourceTypeEnum.app, - resourceModel: MongoApp, - folderTypeList: AppFolderTypeList, - collaborators: parentClbsAndGroups, - session - }); - } else { - logAppMove({ tmbId, teamId, app, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceType: PerResourceTypeEnum.app, teamId: app.teamId, resourceId: app._id }, - { session } - ); - } + const parentClbsAndGroups = await getResourceClbs({ + teamId: app.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.app, + session + }); + // sync self + await syncCollaborators({ + resourceId: app._id, + resourceType: PerResourceTypeEnum.app, + collaborators: parentClbsAndGroups, + session, + teamId: app.teamId + }); + // sync the children + await syncChildrenPermission({ + resource: app, + resourceType: PerResourceTypeEnum.app, + resourceModel: MongoApp, + folderTypeList: AppFolderTypeList, + collaborators: parentClbsAndGroups, + session + }); + logAppMove({ tmbId, teamId, app, targetName }); return onUpdate(session); }); } else { diff --git a/projects/app/src/service/core/app/utils.ts b/projects/app/src/service/core/app/utils.ts index 2d7ba601d8df..228365865e43 100644 --- a/projects/app/src/service/core/app/utils.ts +++ b/projects/app/src/service/core/app/utils.ts @@ -24,6 +24,11 @@ import { type UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { saveChat } from '@fastgpt/service/core/chat/saveChat'; import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller'; import { getErrText } from '@fastgpt/global/common/error/utils'; +import { AppItemType } from '@/types/app'; +import { AppSchema } from '@fastgpt/global/core/app/type'; +import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; export const getScheduleTriggerApp = async () => { addLog.info('Schedule trigger app'); From 805ce907f7e87507467b79f24321fe91e75ad5f0 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 02:31:01 +0800 Subject: [PATCH 06/17] refactor(permission): dataset permission --- packages/global/support/permission/utils.ts | 1 - .../service/support/permission/controller.ts | 1 - .../app/src/pages/api/core/dataset/create.ts | 20 ++++++- .../pages/api/core/dataset/folder/create.ts | 43 +++----------- .../app/src/pages/api/core/dataset/update.ts | 58 ++++++++----------- 5 files changed, 49 insertions(+), 74 deletions(-) diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index 93adf75c1346..abde7f43389e 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -31,7 +31,6 @@ export const checkRoleUpdateConflict = ({ oldRealClbs: CollaboratorItemType[]; newChildClbs: CollaboratorItemType[]; }): boolean => { - console.log('checkRoleUpdateConflict', parentClbs, oldRealClbs, newChildClbs); if (parentClbs.length === 0) { return false; } diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index b5184633272a..336659a6481e 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -468,7 +468,6 @@ export const createResourceDefaultCollaborators = async ({ session: ClientSession; tmbId: string; }) => { - console.log('resource', resource); const parentClbs = await getResourceClbs({ resourceId: resource.parentId, resourceType, diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 71ec9b53a9f0..97e0e0198344 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -2,7 +2,10 @@ import type { CreateDatasetParams } from '@/global/core/dataset/api.d'; import { NextAPI } from '@/service/middleware/entry'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { + PerResourceTypeEnum, + WritePermissionVal +} from '@fastgpt/global/support/permission/constant'; import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils'; @@ -21,6 +24,7 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; export type DatasetCreateQuery = {}; export type DatasetCreateBody = CreateDatasetParams; @@ -71,7 +75,7 @@ async function handler( await checkTeamDatasetLimit(teamId); const datasetId = await mongoSessionRun(async (session) => { - const [{ _id }] = await MongoDataset.create( + const [dataset] = await MongoDataset.create( [ { ...parseParentIdInMongo(parentId), @@ -89,9 +93,19 @@ async function handler( ], { session, ordered: true } ); + + await createResourceDefaultCollaborators({ + folderTypeList: [DatasetTypeEnum.folder], + tmbId, + session, + resource: dataset, + resourceModel: MongoDataset, + resourceType: PerResourceTypeEnum.dataset + }); + await refreshSourceAvatar(avatar, undefined, session); - return _id; + return dataset._id; }); pushTrack.createDataset({ 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 3bb34ad2252e..2e9d71a82013 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -11,10 +11,8 @@ import { import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; +import { createResourceDefaultCollaborators } from '@fastgpt/service/support/permission/controller'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; -import { syncCollaborators } from '@fastgpt/service/support/permission/inheritPermission'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; @@ -62,37 +60,14 @@ async function handler( type: DatasetTypeEnum.folder }); - if (parentId) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); - - await syncCollaborators({ - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - collaborators: parentClbsAndGroups, - session - }); - } - - if (!parentId) { - await MongoResourcePermission.create( - [ - { - resourceType: PerResourceTypeEnum.dataset, - teamId, - resourceId: dataset._id, - tmbId, - permission: OwnerPermissionVal - } - ], - { session, ordered: true } - ); - } + await createResourceDefaultCollaborators({ + folderTypeList: [DatasetTypeEnum.folder], + tmbId, + session, + resource: dataset, + resourceModel: MongoDataset, + resourceType: PerResourceTypeEnum.dataset + }); }); (async () => { addAuditLog({ diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index 0d679c4a09c3..024a73628dc9 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -17,7 +17,6 @@ import { import { type ClientSession } from 'mongoose'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbsAndGroups } from '@fastgpt/service/support/permission/controller'; import { syncChildrenPermission, syncCollaborators @@ -26,10 +25,7 @@ import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; 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'; -import { addDays } from 'date-fns'; import { refreshSourceAvatar } from '@fastgpt/service/common/file/image/controller'; -import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { type DatasetSchemaType } from '@fastgpt/global/core/dataset/type'; import { removeDatasetSyncJobScheduler, @@ -42,6 +38,7 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model'; import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/training/utils'; +import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; @@ -233,39 +230,30 @@ async function handler( await mongoSessionRun(async (session) => { if (isMove) { - if (isFolder && dataset.inheritPermission) { - const parentClbsAndGroups = await getResourceClbsAndGroups({ - teamId: dataset.teamId, - resourceId: parentId, - resourceType: PerResourceTypeEnum.dataset, - session - }); + const parentClbsAndGroups = await getResourceClbs({ + teamId: dataset.teamId, + resourceId: parentId, + resourceType: PerResourceTypeEnum.dataset, + session + }); - await syncCollaborators({ - teamId: dataset.teamId, - resourceId: id, - resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbsAndGroups, - session - }); + await syncCollaborators({ + teamId: dataset.teamId, + resourceId: id, + resourceType: PerResourceTypeEnum.dataset, + collaborators: parentClbsAndGroups, + session + }); - await syncChildrenPermission({ - resource: dataset, - resourceType: PerResourceTypeEnum.dataset, - resourceModel: MongoDataset, - folderTypeList: [DatasetTypeEnum.folder], - collaborators: parentClbsAndGroups, - session - }); - logDatasetMove({ tmbId, teamId, dataset, targetName }); - } else { - logDatasetMove({ tmbId, teamId, dataset, targetName }); - // Not folder, delete all clb - await MongoResourcePermission.deleteMany( - { resourceId: id, teamId: dataset.teamId, resourceType: PerResourceTypeEnum.dataset }, - { session } - ); - } + await syncChildrenPermission({ + resource: dataset, + resourceType: PerResourceTypeEnum.dataset, + resourceModel: MongoDataset, + folderTypeList: [DatasetTypeEnum.folder], + collaborators: parentClbsAndGroups, + session + }); + logDatasetMove({ tmbId, teamId, dataset, targetName }); return onUpdate(session); } else { logDatasetUpdate({ tmbId, teamId, dataset }); From b18e87ab895f9bad56e0a8449a4b22e01e953354 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 03:34:09 +0800 Subject: [PATCH 07/17] refactor(permission): team permission --- .../support/permission/collaborator.d.ts | 2 +- .../service/support/permission/controller.ts | 17 +++++++---- .../permission/MemberManager/context.tsx | 2 +- .../account/team/PermissionManage/index.tsx | 28 ++++++++++++------- projects/app/src/web/support/user/team/api.ts | 12 ++++++-- 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index b8ecc82f7f68..8a873436dbcb 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -30,5 +30,5 @@ export type DeletePermissionQuery = CollaboratorIdType; export type CollaboratorListType = { clbs: CollaboratorItemDetailType[]; - parentClbs: CollaboratorItemDetailType[]; + parentClbs?: CollaboratorItemDetailType[]; }; diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 336659a6481e..18d805ba5f0f 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -119,16 +119,23 @@ export const getResourcePermission = async ({ }; export async function getResourceClbs({ - resourceId, resourceType, teamId, + resourceId, session }: { - resourceId: ParentIdType; - resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; teamId: string; session?: ClientSession; -}) { +} & ( + | { + resourceType: 'team'; + resourceId?: undefined; + } + | { + resourceType: Omit; + resourceId: ParentIdType; + } +)) { return MongoResourcePermission.find( { resourceId, @@ -158,7 +165,7 @@ export const getClbsWithInfo = async ({ resourceId?: undefined; } )) => { - if (!resourceId) { + if (!resourceId && resourceType !== 'team') { return []; } return Promise.all([ diff --git a/projects/app/src/components/support/permission/MemberManager/context.tsx b/projects/app/src/components/support/permission/MemberManager/context.tsx index e3094130ec29..2774f2535e4c 100644 --- a/projects/app/src/components/support/permission/MemberManager/context.tsx +++ b/projects/app/src/components/support/permission/MemberManager/context.tsx @@ -116,7 +116,7 @@ const CollaboratorContextProvider = ({ } = useRequest2( async () => { if (feConfigs.isPlus) { - const { clbs, parentClbs } = await onGetCollaboratorList(); + const { clbs, parentClbs = [] } = await onGetCollaboratorList(); return { clbs: clbs.map((clb) => ({ ...clb, diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index 3feef0e7d0dc..e19e9e66c032 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -19,7 +19,8 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { deleteMemberPermission, getTeamClbs, - updateMemberPermission + updateMemberPermission, + updateOneMemberPermission } from '@/web/support/user/team/api'; import { useUserStore } from '@/web/support/user/useUserStore'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; @@ -51,6 +52,7 @@ import { GetSearchUserGroupOrg } from '@/web/support/user/api'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { type CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; import type { Permission } from '@fastgpt/global/support/permission/controller'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; function PermissionManage({ Tabs, @@ -66,14 +68,14 @@ function PermissionManage({ CollaboratorContext, (state) => state.collaboratorList ); - const onUpdateCollaborators = useContextSelector( - CollaboratorContext, - (state) => state.onUpdateCollaborators - ); const onDelOneCollaborator = useContextSelector( CollaboratorContext, (state) => state.onDelOneCollaborator ); + const refetchCollaborators = useContextSelector( + CollaboratorContext, + (state) => state.refetchCollaboratorList + ); const [isExpandMember, setExpandMember] = useToggle(true); const [isExpandGroup, setExpandGroup] = useToggle(true); @@ -127,12 +129,15 @@ function PermissionManage({ permission.removeRole(per); } - return onUpdateCollaborators({ - ...(clb.tmbId && { members: [clb.tmbId] }), - ...(clb.groupId && { groups: [clb.groupId] }), - ...(clb.orgId && { orgs: [clb.orgId] }), + return updateOneMemberPermission({ + tmbId: clb.tmbId, + groupId: clb.groupId, + orgId: clb.orgId, permission: permission.role }); + }, + { + onSuccess: refetchCollaborators } ); @@ -436,6 +441,7 @@ export const Render = ({ Tabs }: { Tabs: React.ReactNode }) => { return userInfo?.team ? ( { refreshDeps={[userInfo?.team.teamId]} addPermissionOnly={true} > - {({ onOpenAddMember }) => } + {({ onOpenManageModal }) => ( + + )} ) : null; }; diff --git a/projects/app/src/web/support/user/team/api.ts b/projects/app/src/web/support/user/team/api.ts index 5231f1204a5b..5a07ebda02ba 100644 --- a/projects/app/src/web/support/user/team/api.ts +++ b/projects/app/src/web/support/user/team/api.ts @@ -1,6 +1,7 @@ import { GET, POST, PUT, DELETE } from '@/web/common/api/request'; import type { CollaboratorItemType, + CollaboratorListType, DeletePermissionQuery, UpdateClbPermissionProps } from '@fastgpt/global/support/permission/collaborator'; @@ -26,6 +27,7 @@ import type { InvitationLinkCreateType, InvitationType } from '@fastgpt/service/support/user/team/invitationLink/type'; +import type { PermissionValueType } from '@fastgpt/global/support/permission/type'; /* --------------- team ---------------- */ export const getTeamList = (status: `${TeamMemberSchema['status']}`) => @@ -83,9 +85,15 @@ export const putForbidInvitationLink = (linkId: string) => /* -------------- team collaborator -------------------- */ export const getTeamClbs = () => - GET(`/proApi/support/user/team/collaborator/list`); + GET(`/proApi/support/user/team/collaborator/list`); export const updateMemberPermission = (data: UpdateClbPermissionProps) => - PUT('/proApi/support/user/team/collaborator/update', data); + POST('/proApi/support/user/team/collaborator/update', data); +export const updateOneMemberPermission = (data: { + tmbId?: string; + orgId?: string; + groupId?: string; + permission: PermissionValueType; +}) => PUT('/proApi/support/user/team/collaborator/updateOne', data); export const deleteMemberPermission = (id: DeletePermissionQuery) => DELETE('/proApi/support/user/team/collaborator/delete', id); From d2195086d865958e343c1e6e702460794472ee2b Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 12:27:24 +0800 Subject: [PATCH 08/17] chore: fe adjust --- .../permission/MemberManager/ManageModal.tsx | 122 ------------------ .../MemberManager/MemberItemCard.tsx | 76 ++++++----- .../permission/MemberManager/MemberModal.tsx | 12 +- .../permission/MemberManager/RoleSelect.tsx | 36 ++++-- .../permission/MemberManager/RoleTags.tsx | 2 +- .../permission/MemberManager/context.tsx | 7 +- .../account/team/PermissionManage/index.tsx | 8 +- .../pageComponents/dataset/MemberManager.tsx | 11 -- .../src/pageComponents/dataset/list/List.tsx | 4 +- 9 files changed, 85 insertions(+), 193 deletions(-) delete mode 100644 projects/app/src/components/support/permission/MemberManager/ManageModal.tsx diff --git a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx b/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx deleted file mode 100644 index 984d524692a7..000000000000 --- a/projects/app/src/components/support/permission/MemberManager/ManageModal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @deprecated - */ -import { useUserStore } from '@/web/support/user/useUserStore'; -import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; -import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils'; -import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import Avatar from '@fastgpt/web/components/common/Avatar'; -import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import Loading from '@fastgpt/web/components/common/MyLoading'; -import MyModal from '@fastgpt/web/components/common/MyModal'; -import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useTranslation } from 'next-i18next'; -import React from 'react'; -import { useContextSelector } from 'use-context-selector'; -import RoleSelect from './RoleSelect'; -import RoleTags from './RoleTags'; -import { CollaboratorContext } from './context'; -export type ManageModalProps = { - onClose: () => void; -}; - -function ManageModal({ onClose }: ManageModalProps) { - const { t } = useTranslation(); - const { userInfo } = useUserStore(); - const { permission, collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = - useContextSelector(CollaboratorContext, (v) => v); - - const { runAsync: onDelete, loading: isDeleting } = useRequest2(onDelOneCollaborator); - - const { runAsync: onUpdate, loading: isUpdating } = useRequest2(onUpdateCollaborators, { - successToast: t('common:update_success'), - errorToast: 'Error' - }); - - const loading = isDeleting || isUpdating; - - return ( - - - - - - - - - - - - - - {collaboratorList?.map((item) => { - return ( - - - - - - ); - })} - -
{t('user:name')}{t('user:permissions')} - {t('user:operations')} -
- - - {item.name === DefaultGroupName ? userInfo?.team.teamName : item.name} - - - - - {/* Not self; Not owner and other manager */} - {item.tmbId !== userInfo?.team?.tmbId && - (permission.isOwner || !item.permission.hasManagePer) && ( - - } - value={item.permission.role} - onChange={(permission) => { - onUpdate({ - members: item.tmbId ? [item.tmbId] : undefined, - groups: item.groupId ? [item.groupId] : undefined, - orgs: item.orgId ? [item.orgId] : undefined, - permission - }); - }} - onDelete={() => { - onDelete({ - tmbId: item.tmbId, - groupId: item.groupId, - orgId: item.orgId - } as RequireOnlyOne<{ - tmbId: string; - groupId: string; - orgId: string; - }>); - }} - /> - )} -
- {collaboratorList?.length === 0 && } -
- {loading && } -
-
- ); -} - -export default ManageModal; diff --git a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx index e61201ce2a03..f783b47f1a61 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberItemCard.tsx @@ -7,7 +7,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import OrgTags from '../../user/team/OrgTags'; import RoleSelect from './RoleSelect'; import { ChevronDownIcon } from '@chakra-ui/icons'; -import { OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; function MemberItemCard({ avatar, @@ -34,17 +33,18 @@ function MemberItemCard({ rightSlot?: React.ReactNode; disabled?: boolean; }) { + const showRoleSelect = onRoleChange !== undefined; return ( - { @@ -52,56 +52,64 @@ function MemberItemCard({ onChange?.(); }} > - {isChecked !== undefined && } - - - - + + {isChecked !== undefined && } + + {name} {orgs && orgs.length > 0 && } - - {role !== undefined && !!onRoleChange && ( +
+ {showRoleSelect && ( - {role && ( - - - - )} - + + + +
} onChange={onRoleChange} /> )} {onDelete !== undefined && ( - { - if (disabled) return; - onDelete?.(); - }} - /> + + { + if (disabled) return; + onDelete?.(); + }} + /> + )} {rightSlot} -
+ ); } diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 3bf4c3d8496c..1e26caf9809f 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -177,7 +177,7 @@ function MemberModal({ onClose }: { onClose: () => void }) { minW="900px" maxW={'60vw'} h={'100%'} - maxH={'90vh'} + maxH={'50vh'} isCentered isLoading={loadingGroupsAndOrgs} > @@ -188,6 +188,7 @@ function MemberModal({ onClose }: { onClose: () => void }) { borderRadius="0.5rem" gridTemplateColumns="40% 60%" h={'100%'} + gap="4" > void }) { - + {`${t('user:has_chosen')}: `} {editCollaborators.length} - + {editCollaborators.map((clb) => { const onDelete = () => { setCollaboratorList((state) => { @@ -456,6 +457,7 @@ const RenderMemberList = ({ defaultRole: RoleValueType; }) => { const { userInfo } = useUserStore(); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); return ( <> {members?.map((member) => { @@ -487,7 +489,9 @@ const RenderMemberList = ({ isChecked={isChecked} orgs={member.orgs} disabled={ - member.permission.role === OwnerRoleVal || member.tmbId === userInfo?.team.tmbId + member.permission.role === OwnerRoleVal || + member.tmbId === userInfo?.team.tmbId || + (member.permission.hasManagePer && !myRole.isOwner) } /> ); diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index b067b8c594c3..7be9104701e0 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -18,8 +18,7 @@ import { Permission } from '@fastgpt/global/support/permission/controller'; import { CollaboratorContext } from './context'; import { useTranslation } from 'next-i18next'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; -import { useUserStore } from '@/web/support/user/useUserStore'; -import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; +import { ManageRoleVal } from '@fastgpt/global/support/permission/constant'; export type PermissionSelectProps = { value?: RoleValueType; @@ -57,7 +56,7 @@ function RoleSelect({ const closeTimer = useRef(); const { roleList: permissionList } = useContextSelector(CollaboratorContext, (v) => v); - const permission = useContextSelector(CollaboratorContext, (v) => v.permission); + const myRole = useContextSelector(CollaboratorContext, (v) => v.myRole); const [isOpen, setIsOpen] = useState(false); @@ -73,15 +72,18 @@ function RoleSelect({ }; }); + const singleOptions = list.filter((item) => item.checkBoxType === 'single'); + const per = new Permission({ role }); + return { - singleOptions: list.filter( - (item) => - item.checkBoxType === 'single' && - (permission.isOwner || item.value !== permissionList['manage'].value) - ), + singleOptions: myRole.isOwner + ? singleOptions + : myRole.hasManagePer && !per.hasManagePer + ? singleOptions.filter((item) => item.value !== ManageRoleVal) + : [], checkboxList: list.filter((item) => item.checkBoxType === 'multiple') }; - }, [permission.isOwner, permissionList]); + }, [myRole.hasManagePer, myRole.isOwner, permissionList, role]); const selectedSingleValue = useMemo(() => { if (!permissionList) return undefined; @@ -162,6 +164,9 @@ function RoleSelect({ {/* The list of single select permissions */} {roleOptions.singleOptions.map((item) => { const change = () => { + if (disabled) { + return; + } const per = new Permission({ role }); per.removeRole(selectedSingleValue); per.addRole(item.value); @@ -191,7 +196,7 @@ function RoleSelect({ ); })} - {roleOptions.checkboxList.length > 0 && ( + {roleOptions.checkboxList.length > 0 && roleOptions.singleOptions.length > 0 && ( <> @@ -201,7 +206,8 @@ function RoleSelect({ )} {roleOptions.checkboxList.map((item) => { - const change = () => { + const change = (e: React.MouseEvent) => { + if (e.target.tagName === 'INPUT') return; const per = new Permission({ role }); if (per.checkRole(item.value)) { per.removeRole(item.value); @@ -220,9 +226,13 @@ function RoleSelect({ } : {})} {...MenuStyle} + onClick={(e) => { + if (disabled) return; + change(e); + }} > - - + + {t(item.name as any)} {t(item.description as any)} diff --git a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx index fd50190ee54b..849cdbaa1e28 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleTags.tsx @@ -19,7 +19,7 @@ function RoleTags({ permission }: PermissionTagsProp) { const roleTagList = getRoleLabelList(permission); return ( - + {roleTagList.map((item) => ( ({ + myRole: new Permission(), defaultRole: NullRoleVal, collaboratorList: [], parentClbList: [], @@ -180,9 +181,11 @@ const CollaboratorContextProvider = ({ const myRole = useMemo(() => { return ( collaboratorList.find((v) => v.tmbId === userInfo?.team?.tmbId)?.permission ?? - new Permission() + new Permission({ + isOwner: userInfo?.team.permission.isOwner + }) ); - }, [collaboratorList, userInfo?.team?.tmbId]); + }, [collaboratorList, userInfo?.team.permission.isOwner, userInfo?.team?.tmbId]); const contextValue = { permission, diff --git a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx index e19e9e66c032..fe6ece087dc8 100644 --- a/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx +++ b/projects/app/src/pageComponents/account/team/PermissionManage/index.tsx @@ -273,25 +273,25 @@ function PermissionManage({ - - - diff --git a/projects/app/src/pageComponents/dataset/list/List.tsx b/projects/app/src/pageComponents/dataset/list/List.tsx index 43e4282d8687..96844b4de86d 100644 --- a/projects/app/src/pageComponents/dataset/list/List.tsx +++ b/projects/app/src/pageComponents/dataset/list/List.tsx @@ -56,7 +56,7 @@ function List() { const router = useRouter(); const { parentId = null } = router.query as { parentId?: string | null }; const parentDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === parentId), + () => myDatasets.find((item) => item._id === parentId), [parentId, myDatasets] ); @@ -83,7 +83,7 @@ function List() { }); const editPerDataset = useMemo( - () => myDatasets.find((item) => String(item._id) === String(editPerDatasetId)), + () => myDatasets.find((item) => item._id === editPerDatasetId), [editPerDatasetId, myDatasets] ); From 434a53bf308f40d7bd3f9c0c9605a13d8797ffdc Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 15:48:23 +0800 Subject: [PATCH 09/17] fix: type error --- packages/service/common/mongo/index.ts | 1 - .../permission/MemberManager/RoleSelect.tsx | 4 +- .../pageComponents/app/detail/InfoModal.tsx | 60 +++++-------------- .../dataset/detail/Info/index.tsx | 2 + projects/app/src/pages/dataset/list/index.tsx | 2 + .../src/web/core/dataset/api/collaborator.ts | 7 +-- 6 files changed, 24 insertions(+), 52 deletions(-) diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 0def9225fb05..5666d6057464 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -2,7 +2,6 @@ import { isTestEnv } from '@fastgpt/global/common/system/constants'; import { addLog } from '../../common/system/log'; import type { Model } from 'mongoose'; import mongoose, { Mongoose } from 'mongoose'; -import { objectIdToStringPlugin } from './plugin'; export default mongoose; export * from 'mongoose'; diff --git a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx index 7be9104701e0..f8f01012cb39 100644 --- a/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx +++ b/projects/app/src/components/support/permission/MemberManager/RoleSelect.tsx @@ -206,8 +206,8 @@ function RoleSelect({ )} {roleOptions.checkboxList.map((item) => { - const change = (e: React.MouseEvent) => { - if (e.target.tagName === 'INPUT') return; + const change = (e: React.MouseEvent) => { + if ((e.target as HTMLElement).tagName === 'INPUT') return; const per = new Permission({ role }); if (per.checkRole(item.value)) { per.removeRole(item.value); diff --git a/projects/app/src/pageComponents/app/detail/InfoModal.tsx b/projects/app/src/pageComponents/app/detail/InfoModal.tsx index c1ca2cd69017..688324737296 100644 --- a/projects/app/src/pageComponents/app/detail/InfoModal.tsx +++ b/projects/app/src/pageComponents/app/detail/InfoModal.tsx @@ -31,6 +31,8 @@ import { useTranslation } from 'next-i18next'; import React, { useCallback } from 'react'; import { useForm } from 'react-hook-form'; import { useContextSelector } from 'use-context-selector'; +import { postUpdateDatasetCollaborators } from '@/web/core/dataset/api/collaborator'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const InfoModal = ({ onClose }: { onClose: () => void }) => { const { t } = useTranslation(); @@ -100,25 +102,6 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { [handleSubmit, onClose, saveSubmitError, saveSubmitSuccess] ); - const onUpdateCollaborators = ({ - members, - groups, - orgs, - permission - }: { - members?: string[]; - groups?: string[]; - orgs?: string[]; - permission: PermissionValueType; - }) => - postUpdateAppCollaborators({ - members, - groups, - permission, - orgs, - appId: appDetail._id - }); - const onDelCollaborator = async ( props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }> ) => @@ -185,15 +168,14 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { )} getCollaboratorList(appDetail._id)} roleList={AppRoleList} - onUpdateCollaborators={async (props) => - onUpdateCollaborators({ - permission: props.permission, - members: props.members, - groups: props.groups, - orgs: props.orgs + onUpdateCollaborators={async ({ collaborators }) => + postUpdateDatasetCollaborators({ + collaborators, + datasetId: appDetail._id }) } onDelOneCollaborator={onDelCollaborator} @@ -201,7 +183,7 @@ const InfoModal = ({ onClose }: { onClose: () => void }) => { isInheritPermission={appDetail.inheritPermission} hasParent={!!appDetail.parentId} > - {({ MemberListCard, onOpenManageModal, onOpenAddMember }) => { + {({ MemberListCard, onOpenManageModal }) => { return ( <> void }) => { w="full" > {t('common:permission.Collaborator')} - - - - + diff --git a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx index 064a63220ba7..15aabe15c9a5 100644 --- a/projects/app/src/pageComponents/dataset/detail/Info/index.tsx +++ b/projects/app/src/pageComponents/dataset/detail/Info/index.tsx @@ -29,6 +29,7 @@ import dynamic from 'next/dynamic'; import type { EditAPIDatasetInfoFormType } from './components/EditApiServiceModal'; import { type EditResourceInfoFormType } from '@/components/common/Modal/EditResourceModal'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; +import { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditResourceModal = dynamic(() => import('@/components/common/Modal/EditResourceModal')); const EditAPIDatasetInfoModal = dynamic(() => import('./components/EditApiServiceModal')); @@ -380,6 +381,7 @@ const Info = ({ datasetId }: { datasetId: string }) => { getCollaboratorList(datasetId), roleList: DatasetRoleList, diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index 4b8b5759e776..dedb5f17f8fb 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -30,6 +30,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 { ReadRoleVal } from '@fastgpt/global/support/permission/constant'; const EditFolderModal = dynamic( () => import('@fastgpt/web/components/common/MyModal/EditFolderModal') @@ -254,6 +255,7 @@ const Dataset = () => { }) } managePer={{ + defaultRole: ReadRoleVal, permission: folderDetail.permission, onGetCollaboratorList: () => getCollaboratorList(folderDetail._id), roleList: DatasetRoleList, diff --git a/projects/app/src/web/core/dataset/api/collaborator.ts b/projects/app/src/web/core/dataset/api/collaborator.ts index 91f83eaf9910..6a8336805cf8 100644 --- a/projects/app/src/web/core/dataset/api/collaborator.ts +++ b/projects/app/src/web/core/dataset/api/collaborator.ts @@ -3,13 +3,10 @@ import type { DatasetCollaboratorDeleteParams } from '@fastgpt/global/core/dataset/collaborator'; import { DELETE, GET, POST } from '@/web/common/api/request'; -import type { - CollaboratorItemDetailType, - CollaboratorItemType -} from '@fastgpt/global/support/permission/collaborator'; +import type { CollaboratorListType } from '@fastgpt/global/support/permission/collaborator'; export const getCollaboratorList = (datasetId: string) => - GET('/proApi/core/dataset/collaborator/list', { datasetId }); + GET('/proApi/core/dataset/collaborator/list', { datasetId }); export const postUpdateDatasetCollaborators = (body: UpdateDatasetCollaboratorBody) => POST('/proApi/core/dataset/collaborator/update', body); From 2072d093ebb96d5e189d8aaf6edba9361710b849 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 20:26:55 +0800 Subject: [PATCH 10/17] fix: audit pagiation --- projects/app/src/pageComponents/account/team/Audit/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/app/src/pageComponents/account/team/Audit/index.tsx b/projects/app/src/pageComponents/account/team/Audit/index.tsx index 3174790ca9b9..8870552ff01d 100644 --- a/projects/app/src/pageComponents/account/team/Audit/index.tsx +++ b/projects/app/src/pageComponents/account/team/Audit/index.tsx @@ -72,7 +72,7 @@ function AuditLog({ Tabs }: { Tabs: React.ReactNode }) { isLoading: loadingLogs, ScrollData: LogScrollData } = useScrollPagination(getOperationLogs, { - pageSize: 20, + pageSize: 30, refreshDeps: [searchParams], params: searchParams }); From ec4faf688796791c2c9ae361bb88e15a4d484351 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 25 Aug 2025 20:47:28 +0800 Subject: [PATCH 11/17] fix: tc --- .../service/support/permission/controller.ts | 75 ++++--------------- projects/app/src/pages/api/core/app/create.ts | 2 - .../src/pages/api/core/app/folder/create.ts | 2 - .../app/src/pages/api/core/dataset/create.ts | 2 - .../pages/api/core/dataset/folder/create.ts | 3 - projects/app/test/api/core/app/create.test.ts | 38 ++++++---- .../training/getTrainingDataDetail.test.ts | 4 +- .../global/support/permission/utils.test.ts | 18 ++--- 8 files changed, 50 insertions(+), 94 deletions(-) diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 18d805ba5f0f..c8f2d2a705e6 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,13 +1,12 @@ import Cookie from 'cookie'; -import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; +import type { ClientSession, AnyBulkWriteOperation } from '../../common/mongo'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import jwt from 'jsonwebtoken'; -import { type NextApiResponse, type NextApiRequest } from 'next'; +import { type NextApiResponse } from 'next'; import type { AuthModeType, ReqHeaderAuthType } from './type.d'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { AuthUserTypeEnum, - ManagePermissionVal, ManageRoleVal, NullRoleVal, OwnerRoleVal @@ -16,7 +15,7 @@ import { authOpenApiKey } from '../openapi/auth'; import { type FileTokenQuery } from '@fastgpt/global/common/file/type'; import { MongoResourcePermission } from './schema'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; -import { ResourceType, type PermissionValueType } from '@fastgpt/global/support/permission/type'; +import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { bucketNameMap } from '@fastgpt/global/common/file/constants'; import { addMinutes } from 'date-fns'; import { getGroupsByTmbId } from './memberGroup/controllers'; @@ -28,12 +27,11 @@ import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; import { authUserSession } from '../user/session'; -import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; +import { sumPer } from '@fastgpt/global/support/permission/utils'; import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; -import { syncCollaborators, type SyncChildrenPermissionResourceType } from './inheritPermission'; +import { type SyncChildrenPermissionResourceType } from './inheritPermission'; import { pickCollaboratorIdFields } from './utils'; import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; -import { CollaboratorIdType } from '@fastgpt/global/support/permission/collaborator'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -457,18 +455,11 @@ export const authFileToken = (token?: string) => export const createResourceDefaultCollaborators = async ({ resource, - folderTypeList, resourceType, - resourceModel, session, tmbId }: { resource: SyncChildrenPermissionResourceType; - - // when the resource is a folder - folderTypeList: string[]; - - resourceModel: typeof Model; resourceType: PerResourceTypeEnum; // should be provided when inheritPermission is true @@ -504,59 +495,23 @@ export const createResourceDefaultCollaborators = async ({ for (const clb of collaborators) { ops.push({ - insertOne: { - document: { + updateOne: { + filter: { ...pickCollaboratorIdFields(clb), teamId: resource.teamId, resourceId: resource._id, - permission: clb.permission, - selfPermission: NullRoleVal, resourceType - } as ResourcePermissionType + }, + update: { + $set: { + permission: clb.permission, + selfPermission: NullRoleVal + } + }, + upsert: true } }); } - // for (const parentClb of parentClbs) { - // ops.push({ - // insertOne: { - // document: { - // ...pickCollaboratorIdFields(parentClb), - // teamId: resource.teamId, - // resourceId: resource._id, - // permission: parentClb.permission === OwnerRoleVal ? ManageRoleVal : parentClb.permission, - // selfPermission: NullRoleVal, - // resourceType - // } as ResourcePermissionType - // } - // }); - // } - - // ops.push({ - // updateOne: { - // filter: { - // resourceId: resource._id, - // teamId: resource.teamId, - // resourceType, - // tmbId - // }, - // update: { - // permission: OwnerRoleVal, - // selfPermission: OwnerRoleVal - // }, - // upsert: true - // } - // }); - await MongoResourcePermission.bulkWrite(ops, { session }); - - // if (resource.type in folderTypeList) { - // await syncCollaborators({ - // collaborators, - // resourceId: resource._id, - // resourceType, - // session, - // teamId: resource.teamId - // }); - // } }; diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index 20a2915b7996..dda285c0cc90 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -159,11 +159,9 @@ export const onCreateApp = async ({ } await createResourceDefaultCollaborators({ - folderTypeList: AppFolderTypeList, tmbId, session, resource: app, - resourceModel: MongoApp, resourceType: PerResourceTypeEnum.app }); (async () => { 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 2ef4f9a67cf3..b986a7b19812 100644 --- a/projects/app/src/pages/api/core/app/folder/create.ts +++ b/projects/app/src/pages/api/core/app/folder/create.ts @@ -48,11 +48,9 @@ async function handler(req: ApiRequestProps) { }); await createResourceDefaultCollaborators({ - folderTypeList: AppFolderTypeList, tmbId, session, resource: app, - resourceModel: MongoApp, resourceType: PerResourceTypeEnum.app }); }); diff --git a/projects/app/src/pages/api/core/dataset/create.ts b/projects/app/src/pages/api/core/dataset/create.ts index 97e0e0198344..0fae1212e3f7 100644 --- a/projects/app/src/pages/api/core/dataset/create.ts +++ b/projects/app/src/pages/api/core/dataset/create.ts @@ -95,11 +95,9 @@ async function handler( ); await createResourceDefaultCollaborators({ - folderTypeList: [DatasetTypeEnum.folder], tmbId, session, resource: dataset, - resourceModel: MongoDataset, resourceType: PerResourceTypeEnum.dataset }); 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 2e9d71a82013..426db0a7f864 100644 --- a/projects/app/src/pages/api/core/dataset/folder/create.ts +++ b/projects/app/src/pages/api/core/dataset/folder/create.ts @@ -4,7 +4,6 @@ import { FolderImgUrl } from '@fastgpt/global/common/file/image/constants'; import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'; import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; import { - OwnerPermissionVal, PerResourceTypeEnum, WritePermissionVal } from '@fastgpt/global/support/permission/constant'; @@ -61,11 +60,9 @@ async function handler( }); await createResourceDefaultCollaborators({ - folderTypeList: [DatasetTypeEnum.folder], tmbId, session, resource: dataset, - resourceModel: MongoDataset, resourceType: PerResourceTypeEnum.dataset }); }); diff --git a/projects/app/test/api/core/app/create.test.ts b/projects/app/test/api/core/app/create.test.ts index 0e470a1d9772..69ae40d7158b 100644 --- a/projects/app/test/api/core/app/create.test.ts +++ b/projects/app/test/api/core/app/create.test.ts @@ -11,13 +11,18 @@ import { describe, expect, it } 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 - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'team', + teamId: users.members[0].teamId, + resourceId: null, + tmbId: users.members[0].tmbId + }, + { + permission: TeamAppCreatePermissionVal + }, + { upsert: true } + ); const res = await Call(createapi.default, { auth: users.members[0], @@ -56,13 +61,18 @@ describe('create api', () => { expect(res3.error).toBe(AppErrEnum.unAuthApp); expect(res3.code).toBe(500); - await MongoResourcePermission.create({ - resourceType: 'app', - teamId: users.members[1].teamId, - resourceId: String(folderId), - tmbId: users.members[1].tmbId, - permission: WritePermissionVal - }); + await MongoResourcePermission.findOneAndUpdate( + { + resourceType: 'app', + teamId: users.members[1].teamId, + resourceId: String(folderId), + tmbId: users.members[1].tmbId + }, + { + permission: WritePermissionVal + }, + { upsert: true } + ); const res4 = await Call(createapi.default, { auth: users.members[1], diff --git a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts index d4a0b2793ec5..2c3f672bc29e 100644 --- a/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts +++ b/projects/app/test/api/core/dataset/training/getTrainingDataDetail.test.ts @@ -51,8 +51,8 @@ describe('get training data detail test', () => { expect(res.code).toBe(200); expect(res.data).toBeDefined(); - expect(res.data?._id).toStrictEqual(trainingData._id); - expect(res.data?.datasetId).toStrictEqual(dataset._id); + expect(res.data?._id).toStrictEqual(String(trainingData._id)); + expect(res.data?.datasetId).toStrictEqual(String(dataset._id)); expect(res.data?.mode).toBe(TrainingModeEnum.chunk); expect(res.data?.q).toBe('test'); expect(res.data?.a).toBe('test'); diff --git a/test/cases/global/support/permission/utils.test.ts b/test/cases/global/support/permission/utils.test.ts index 1b55822fcdc7..2048ba1301bf 100644 --- a/test/cases/global/support/permission/utils.test.ts +++ b/test/cases/global/support/permission/utils.test.ts @@ -5,7 +5,7 @@ describe('Test checkRoleUpdateConflict', () => { it('There is no any old collaborator, should return false', () => { const result = checkRoleUpdateConflict({ parentClbs: [], - oldChildClbs: [], + oldRealClbs: [], newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); @@ -13,7 +13,7 @@ describe('Test checkRoleUpdateConflict', () => { it('There is no parent collaborator, should return false', () => { const result = checkRoleUpdateConflict({ parentClbs: [], - oldChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], newChildClbs: [{ permission: 0b001, tmbId: 'fakeTmbId2' }] }); expect(result).toBe(false); @@ -21,7 +21,7 @@ describe('Test checkRoleUpdateConflict', () => { it("Edit parent's permission, should return true", () => { const result = checkRoleUpdateConflict({ parentClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], - oldChildClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b011, tmbId: 'fakeTmbId1' }], newChildClbs: [{ permission: 0b010, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(true); @@ -29,7 +29,7 @@ describe('Test checkRoleUpdateConflict', () => { it("Edit permission but parent's permission bit is not set, should return false", () => { const result = checkRoleUpdateConflict({ parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], - oldChildClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); @@ -37,7 +37,7 @@ describe('Test checkRoleUpdateConflict', () => { it('add new clb, should return false', () => { const result = checkRoleUpdateConflict({ parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], - oldChildClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], + oldRealClbs: [{ permission: 0b1111, tmbId: 'fakeTmbId1' }], newChildClbs: [ { permission: 0b1111, tmbId: 'fakeTmbId1' }, { permission: 0b1001, tmbId: 'fakeTmbId2' } @@ -45,18 +45,18 @@ describe('Test checkRoleUpdateConflict', () => { }); expect(result).toBe(false); }); - it('add clb, no oldChildClbs', () => { + it('add clb, no oldRealClbs', () => { const result = checkRoleUpdateConflict({ parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], - oldChildClbs: [], + oldRealClbs: [], newChildClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(true); }); - it('add clb, no oldChildClbs, false', () => { + it('add clb, no oldRealClbs, false', () => { const result = checkRoleUpdateConflict({ parentClbs: [{ permission: 0b1001, tmbId: 'fakeTmbId1' }], - oldChildClbs: [], + oldRealClbs: [], newChildClbs: [{ permission: 0b0110, tmbId: 'fakeTmbId1' }] }); expect(result).toBe(false); From a2d885221302fb5ac3200c25ba9507b9ceb03dc4 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Tue, 26 Aug 2025 10:45:10 +0800 Subject: [PATCH 12/17] chore: initv4130 --- projects/app/src/pages/api/admin/initv4130.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 projects/app/src/pages/api/admin/initv4130.ts diff --git a/projects/app/src/pages/api/admin/initv4130.ts b/projects/app/src/pages/api/admin/initv4130.ts new file mode 100644 index 000000000000..2fe8ce826001 --- /dev/null +++ b/projects/app/src/pages/api/admin/initv4130.ts @@ -0,0 +1,118 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { NextAPI } from '@/service/middleware/entry'; +import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { MongoApp } from '@fastgpt/service/core/app/schema'; +import { AppFolderTypeList } from '@fastgpt/global/core/app/constants'; +import { syncChildrenPermission } from '@fastgpt/service/support/permission/inheritPermission'; +import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; +import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; +import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; +import { addLog } from '@fastgpt/service/common/system/log'; +import { MongoDataset } from '@fastgpt/service/core/dataset/schema'; +import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants'; + +export type SyncAppChatLogQuery = {}; + +export type SyncAppChatLogBody = { + batchSize?: number; +}; + +export type SyncAppChatLogResponse = {}; + +/** + * 初始化脚本 v4.13.0 + * 权限表数据更新:直接对系统内所有资源(没有 parentId 顶层的 Folder,以及没有 inheritPermission 的有 parentId 的Folder),执行一次 syncChildrenPermission 函数。 + * 包括:App 和 Dataset 的权限同步 + */ +async function handler( + req: ApiRequestProps, + res: ApiResponseType +) { + await authCert({ req, authRoot: true }); + // 1. update App's + const appFolders = await MongoApp.find({ + $or: [ + { + parentId: null, + type: { $in: AppFolderTypeList } + }, + { + parentId: { $exists: true }, + inheritPermission: false, + type: { $in: AppFolderTypeList } + } + ] + }).lean(); + addLog.info(`start sync app children permission, total: ${appFolders.length}`); + for (const folder of appFolders) { + await mongoSessionRun(async (session) => { + const clbs = await getResourceClbs({ + resourceType: PerResourceTypeEnum.app, + resourceId: folder._id, + teamId: folder.teamId, + session + }); + await syncChildrenPermission({ + folderTypeList: AppFolderTypeList, + resource: folder, + session, + resourceModel: MongoApp, + resourceType: PerResourceTypeEnum.app, + collaborators: clbs + }); + }); + addLog.debug( + `sync app children permission, folderId: ${folder._id}, progress: ${appFolders.indexOf(folder) + 1}/${appFolders.length}` + ); + } + addLog.info('sync app children permission completed'); + + // 2. update Dataset's + const datasetFolders = await MongoDataset.find({ + $or: [ + { + parentId: null, + type: DatasetTypeEnum.folder + }, + { + parentId: { $exists: true }, + inheritPermission: false, + type: DatasetTypeEnum.folder + } + ] + }).lean(); + + addLog.info(`start sync dataset children permission, total: ${datasetFolders.length}`); + + for (const folder of datasetFolders) { + await mongoSessionRun(async (session) => { + const clbs = await getResourceClbs({ + resourceType: PerResourceTypeEnum.dataset, + resourceId: folder._id, + teamId: folder.teamId, + session + }); + + await syncChildrenPermission({ + folderTypeList: [DatasetTypeEnum.folder], + resource: folder, + session, + resourceModel: MongoDataset, + resourceType: PerResourceTypeEnum.dataset, + collaborators: clbs + }); + }); + + addLog.debug( + `sync dataset children permission, folderId: ${folder._id}, progress: ${datasetFolders.indexOf(folder) + 1}/${datasetFolders.length}` + ); + } + + addLog.info('sync dataset children permission completed'); + + return { + message: 'App and Dataset permission sync completed successfully' + }; +} + +export default NextAPI(handler); From f78338bea8653eea534e3d3038996a4d4b9ce278 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Tue, 26 Aug 2025 18:10:01 +0800 Subject: [PATCH 13/17] fix: app/dataset auth logic --- .../service/support/permission/app/auth.ts | 56 +++---------------- .../support/permission/dataset/auth.ts | 55 +++--------------- .../permission/MemberManager/MemberModal.tsx | 5 +- 3 files changed, 20 insertions(+), 96 deletions(-) diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 0accefdc7d7d..8eb53bd70bd5 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -90,53 +90,15 @@ export const authAppByTmbId = async ({ const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new AppPermission({ isOwner: true }) - }; - } - - if ( - AppFolderTypeList.includes(app.type) || - app.inheritPermission === false || - !app.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/app. - const role = await getResourcePermission({ - teamId, - tmbId, - resourceId: appId, - resourceType: PerResourceTypeEnum.app - }); - const Per = new AppPermission({ role, isOwner }); - - if (app.favourite || app.quick) { - Per.addRole(ReadRoleVal); - } - - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { app: parent } = await authAppByTmbId({ - tmbId, - appId: app.parentId, - per - }); - - const Per = new AppPermission({ - role: parent.permission.role, - isOwner - }); - return { - Per - }; - } - })(); + const Per = new AppPermission({ + role: await getResourcePermission({ + teamId, + tmbId, + resourceId: appId, + resourceType: PerResourceTypeEnum.app + }), + isOwner + }); if (!Per.checkPer(per)) { return Promise.reject(AppErrEnum.unAuthApp); diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 3b4ca9d8358f..9e5bcef071ab 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -63,52 +63,15 @@ export const authDatasetByTmbId = async ({ const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); // get dataset permission or inherit permission from parent folder. - const { Per } = await (async () => { - if (isOwner) { - return { - Per: new DatasetPermission({ isOwner: true }) - }; - } - if ( - dataset.type === DatasetTypeEnum.folder || - dataset.inheritPermission === false || - !dataset.parentId - ) { - // 1. is a folder. (Folders have completely permission) - // 2. inheritPermission is false. - // 3. is root folder/dataset. - const rp = await getResourcePermission({ - teamId, - tmbId, - resourceId: datasetId, - resourceType: PerResourceTypeEnum.dataset - }); - const Per = new DatasetPermission({ - role: rp, - isOwner - }); - return { - Per - }; - } else { - // is not folder and inheritPermission is true and is not root folder. - const { dataset: parent } = await authDatasetByTmbId({ - tmbId, - datasetId: dataset.parentId, - per, - isRoot - }); - - const Per = new DatasetPermission({ - role: parent.permission.role, - isOwner - }); - - return { - Per - }; - } - })(); + const Per = new DatasetPermission({ + role: await getResourcePermission({ + teamId, + tmbId, + resourceId: datasetId, + resourceType: PerResourceTypeEnum.dataset + }), + isOwner + }); if (!Per.checkPer(per)) { return Promise.reject(DatasetErrEnum.unAuthDataset); diff --git a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx index 1e26caf9809f..6d4d3a3492f8 100644 --- a/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx +++ b/projects/app/src/components/support/permission/MemberManager/MemberModal.tsx @@ -188,14 +188,13 @@ function MemberModal({ onClose }: { onClose: () => void }) { borderRadius="0.5rem" gridTemplateColumns="40% 60%" h={'100%'} - gap="4" > void }) { - + {`${t('user:has_chosen')}: `} {editCollaborators.length} From c6e034c33558733e195f54c2200caa6ae0825bb1 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Wed, 3 Sep 2025 12:08:23 +0800 Subject: [PATCH 14/17] chore: move code --- packages/service/common/response/index.ts | 2 +- .../service/support/permission/app/auth.ts | 2 +- .../service/support/permission/auth/common.ts | 154 +++++++++++++- .../service/support/permission/auth/file.ts | 50 ++++- .../support/permission/auth/openapi.ts | 2 +- .../service/support/permission/controller.ts | 201 ------------------ .../support/permission/dataset/auth.ts | 3 +- .../permission/memberGroup/controllers.ts | 2 +- .../support/permission/publish/authLink.ts | 2 +- .../service/support/permission/user/auth.ts | 3 +- .../app/src/pages/api/common/file/read.ts | 2 +- .../pages/api/common/file/read/[filename].ts | 2 +- .../app/src/pages/api/common/file/upload.ts | 2 +- .../pages/api/core/dataset/collection/read.ts | 4 +- .../support/user/account/loginByPassword.ts | 2 +- .../api/support/user/account/loginout.ts | 3 +- .../user/account/updatePasswordByOld.ts | 1 - 17 files changed, 216 insertions(+), 221 deletions(-) diff --git a/packages/service/common/response/index.ts b/packages/service/common/response/index.ts index 5d2f58db6ea8..350a028472eb 100644 --- a/packages/service/common/response/index.ts +++ b/packages/service/common/response/index.ts @@ -2,9 +2,9 @@ import type { NextApiResponse } from 'next'; import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants'; import { proxyError, ERROR_RESPONSE, ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { addLog } from '../system/log'; -import { clearCookie } from '../../support/permission/controller'; import { replaceSensitiveText } from '@fastgpt/global/common/string/tools'; import { UserError } from '@fastgpt/global/common/error/utils'; +import { clearCookie } from '../../support/permission/auth/common'; export interface ResponseType { code: number; diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 8eb53bd70bd5..38098642da38 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -1,7 +1,6 @@ /* Auth app permission */ import { MongoApp } from '../../../core/app/schema'; import { type AppDetailType } from '@fastgpt/global/core/app/type.d'; -import { parseHeaderCert } from '../controller'; import { PerResourceTypeEnum, ReadPermissionVal, @@ -18,6 +17,7 @@ import { PluginSourceEnum } from '@fastgpt/global/core/app/plugin/constants'; import { type AuthModeType, type AuthResponseType } from '../type'; import { splitCombinePluginId } from '@fastgpt/global/core/app/plugin/utils'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; +import { parseHeaderCert } from '../auth/common'; export const authPluginByTmbId = async ({ tmbId, diff --git a/packages/service/support/permission/auth/common.ts b/packages/service/support/permission/auth/common.ts index 31b0b13cdb20..23f40362a406 100644 --- a/packages/service/support/permission/auth/common.ts +++ b/packages/service/support/permission/auth/common.ts @@ -1,7 +1,13 @@ -import { parseHeaderCert } from '../controller'; +import type { ReqHeaderAuthType } from '../type'; import { type AuthModeType } from '../type'; import { SERVICE_LOCAL_HOST } from '../../../common/system/tools'; import { type ApiRequestProps } from '../../../type/next'; +import type { NextApiResponse } from 'next'; +import Cookie from 'cookie'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; +import { authUserSession } from '../../../support/user/session'; +import { authOpenApiKey } from '../../../support/openapi/auth'; +import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; export const authCert = async (props: AuthModeType) => { const result = await parseHeaderCert(props); @@ -19,3 +25,149 @@ export const authRequestFromLocal = ({ req }: { req: ApiRequestProps }) => { return Promise.reject('Invalid request'); } }; + +export async function parseHeaderCert({ + req, + authToken = false, + authRoot = false, + authApiKey = false +}: AuthModeType) { + // parse jwt + async function authCookieToken(cookie?: string, token?: string) { + // 获取 cookie + const cookies = Cookie.parse(cookie || ''); + const cookieToken = token || cookies[TokenName]; + + if (!cookieToken) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; + } + // from authorization get apikey + async function parseAuthorization(authorization?: string) { + if (!authorization) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + // Bearer fastgpt-xxxx-appId + const auth = authorization.split(' ')[1]; + if (!auth) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + const { apikey, appId: authorizationAppid = '' } = await (async () => { + const arr = auth.split('-'); + // abandon + if (arr.length === 3) { + return { + apikey: `${arr[0]}-${arr[1]}`, + appId: arr[2] + }; + } + if (arr.length === 2) { + return { + apikey: auth + }; + } + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + // auth apikey + const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); + + return { + uid: '', + teamId, + tmbId, + apikey, + appId: apiKeyAppId || authorizationAppid, + sourceName + }; + } + // root user + async function parseRootKey(rootKey?: string) { + if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + } + + const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; + + const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = + await (async () => { + if (authApiKey && authorization) { + // apikey from authorization + const authResponse = await parseAuthorization(authorization); + return { + uid: authResponse.uid, + teamId: authResponse.teamId, + tmbId: authResponse.tmbId, + appId: authResponse.appId, + openApiKey: authResponse.apikey, + authType: AuthUserTypeEnum.apikey, + sourceName: authResponse.sourceName + }; + } + if (authToken && (token || cookie)) { + // user token(from fastgpt web) + const res = await authCookieToken(cookie, token); + + return { + uid: res.userId, + teamId: res.teamId, + tmbId: res.tmbId, + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.token, + isRoot: res.isRoot, + sessionId: res.sessionId + }; + } + if (authRoot && rootkey) { + await parseRootKey(rootkey); + // root user + return { + uid: '', + teamId: '', + tmbId: '', + appId: '', + openApiKey: '', + authType: AuthUserTypeEnum.root, + isRoot: true + }; + } + + return Promise.reject(ERROR_ENUM.unAuthorization); + })(); + + if (!authRoot && (!teamId || !tmbId)) { + return Promise.reject(ERROR_ENUM.unAuthorization); + } + + return { + userId: String(uid), + teamId: String(teamId), + tmbId: String(tmbId), + appId, + authType, + sourceName, + apikey: openApiKey, + isRoot: !!isRoot, + sessionId + }; +} + +/* set cookie */ +export const TokenName = 'fastgpt_token'; +export const setCookie = (res: NextApiResponse, token: string) => { + res.setHeader( + 'Set-Cookie', + `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` + ); +}; + +/* clear cookie */ +export const clearCookie = (res: NextApiResponse) => { + res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); +}; diff --git a/packages/service/support/permission/auth/file.ts b/packages/service/support/permission/auth/file.ts index 6ea324d04466..bc4d868072a4 100644 --- a/packages/service/support/permission/auth/file.ts +++ b/packages/service/support/permission/auth/file.ts @@ -1,11 +1,15 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type DatasetFileSchema } from '@fastgpt/global/core/dataset/type'; -import { parseHeaderCert } from '../controller'; import { getFileById } from '../../../common/file/gridfs/controller'; -import { BucketNameEnum } from '@fastgpt/global/common/file/constants'; +import { BucketNameEnum, bucketNameMap } from '@fastgpt/global/common/file/constants'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { OwnerPermissionVal, ReadRoleVal } from '@fastgpt/global/support/permission/constant'; import { Permission } from '@fastgpt/global/support/permission/controller'; +import type { FileTokenQuery } from '@fastgpt/global/common/file/type'; +import { addMinutes } from 'date-fns'; +import { parseHeaderCert } from './common'; +import jwt from 'jsonwebtoken'; +import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; export const authCollectionFile = async ({ fileId, @@ -46,3 +50,45 @@ export const authCollectionFile = async ({ file }; }; + +/* file permission */ +export const createFileToken = (data: FileTokenQuery) => { + if (!process.env.FILE_TOKEN_KEY) { + return Promise.reject('System unset FILE_TOKEN_KEY'); + } + + const expireMinutes = + data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; + const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); + + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + const token = jwt.sign( + { + ...data, + exp: expiredTime + }, + key + ); + return Promise.resolve(token); +}; + +export const authFileToken = (token?: string) => + new Promise((resolve, reject) => { + if (!token) { + return reject(ERROR_ENUM.unAuthFile); + } + const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; + + jwt.verify(token, key, (err, decoded: any) => { + if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { + reject(ERROR_ENUM.unAuthFile); + return; + } + resolve({ + bucketName: decoded.bucketName, + teamId: decoded.teamId, + uid: decoded.uid, + fileId: decoded.fileId + }); + }); + }); diff --git a/packages/service/support/permission/auth/openapi.ts b/packages/service/support/permission/auth/openapi.ts index 206eff13d8b7..a2a404fce4a5 100644 --- a/packages/service/support/permission/auth/openapi.ts +++ b/packages/service/support/permission/auth/openapi.ts @@ -1,11 +1,11 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { type OpenApiSchema } from '@fastgpt/global/support/openapi/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { MongoOpenApi } from '../../openapi/schema'; import { OpenApiErrEnum } from '@fastgpt/global/common/error/code/openapi'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; +import { parseHeaderCert } from './common'; export async function authOpenApiKeyCrud({ id, diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index c8f2d2a705e6..293ce9e1982b 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,23 +1,13 @@ -import Cookie from 'cookie'; import type { ClientSession, AnyBulkWriteOperation } from '../../common/mongo'; -import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; -import jwt from 'jsonwebtoken'; -import { type NextApiResponse } from 'next'; -import type { AuthModeType, ReqHeaderAuthType } from './type.d'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { - AuthUserTypeEnum, ManageRoleVal, NullRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; -import { authOpenApiKey } from '../openapi/auth'; -import { type FileTokenQuery } from '@fastgpt/global/common/file/type'; import { MongoResourcePermission } from './schema'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } 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 { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; @@ -26,7 +16,6 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/m import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; -import { authUserSession } from '../user/session'; import { sumPer } from '@fastgpt/global/support/permission/utils'; import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; import { type SyncChildrenPermissionResourceType } from './inheritPermission'; @@ -263,196 +252,6 @@ export const delResourcePermission = ({ ); }; -/* 下面代码等迁移 */ - -export async function parseHeaderCert({ - req, - authToken = false, - authRoot = false, - authApiKey = false -}: AuthModeType) { - // parse jwt - async function authCookieToken(cookie?: string, token?: string) { - // 获取 cookie - const cookies = Cookie.parse(cookie || ''); - const cookieToken = token || cookies[TokenName]; - - if (!cookieToken) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { ...(await authUserSession(cookieToken)), sessionId: cookieToken }; - } - // from authorization get apikey - async function parseAuthorization(authorization?: string) { - if (!authorization) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - // Bearer fastgpt-xxxx-appId - const auth = authorization.split(' ')[1]; - if (!auth) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - const { apikey, appId: authorizationAppid = '' } = await (async () => { - const arr = auth.split('-'); - // abandon - if (arr.length === 3) { - return { - apikey: `${arr[0]}-${arr[1]}`, - appId: arr[2] - }; - } - if (arr.length === 2) { - return { - apikey: auth - }; - } - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); - - // auth apikey - const { teamId, tmbId, appId: apiKeyAppId = '', sourceName } = await authOpenApiKey({ apikey }); - - return { - uid: '', - teamId, - tmbId, - apikey, - appId: apiKeyAppId || authorizationAppid, - sourceName - }; - } - // root user - async function parseRootKey(rootKey?: string) { - if (!rootKey || !process.env.ROOT_KEY || rootKey !== process.env.ROOT_KEY) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - } - - const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; - - const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } = - await (async () => { - if (authApiKey && authorization) { - // apikey from authorization - const authResponse = await parseAuthorization(authorization); - return { - uid: authResponse.uid, - teamId: authResponse.teamId, - tmbId: authResponse.tmbId, - appId: authResponse.appId, - openApiKey: authResponse.apikey, - authType: AuthUserTypeEnum.apikey, - sourceName: authResponse.sourceName - }; - } - if (authToken && (token || cookie)) { - // user token(from fastgpt web) - const res = await authCookieToken(cookie, token); - - return { - uid: res.userId, - teamId: res.teamId, - tmbId: res.tmbId, - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.token, - isRoot: res.isRoot, - sessionId: res.sessionId - }; - } - if (authRoot && rootkey) { - await parseRootKey(rootkey); - // root user - return { - uid: '', - teamId: '', - tmbId: '', - appId: '', - openApiKey: '', - authType: AuthUserTypeEnum.root, - isRoot: true - }; - } - - return Promise.reject(ERROR_ENUM.unAuthorization); - })(); - - if (!authRoot && (!teamId || !tmbId)) { - return Promise.reject(ERROR_ENUM.unAuthorization); - } - - return { - userId: String(uid), - teamId: String(teamId), - tmbId: String(tmbId), - appId, - authType, - sourceName, - apikey: openApiKey, - isRoot: !!isRoot, - sessionId - }; -} - -/* set cookie */ -export const TokenName = 'fastgpt_token'; -export const setCookie = (res: NextApiResponse, token: string) => { - res.setHeader( - 'Set-Cookie', - `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` - ); -}; - -/* clear cookie */ -export const clearCookie = (res: NextApiResponse) => { - res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); -}; - -/* file permission */ -export const createFileToken = (data: FileTokenQuery) => { - if (!process.env.FILE_TOKEN_KEY) { - return Promise.reject('System unset FILE_TOKEN_KEY'); - } - - const expireMinutes = - data.customExpireMinutes ?? bucketNameMap[data.bucketName].previewExpireMinutes; - const expiredTime = Math.floor(addMinutes(new Date(), expireMinutes).getTime() / 1000); - - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - const token = jwt.sign( - { - ...data, - exp: expiredTime - }, - key - ); - return Promise.resolve(token); -}; - -export const authFileToken = (token?: string) => - new Promise((resolve, reject) => { - if (!token) { - return reject(ERROR_ENUM.unAuthFile); - } - const key = (process.env.FILE_TOKEN_KEY as string) ?? 'filetoken'; - - jwt.verify(token, key, (err, decoded: any) => { - if (err || !decoded.bucketName || !decoded?.teamId || !decoded?.fileId) { - reject(ERROR_ENUM.unAuthFile); - return; - } - resolve({ - bucketName: decoded.bucketName, - teamId: decoded.teamId, - uid: decoded.uid, - fileId: decoded.fileId - }); - }); - }); - export const createResourceDefaultCollaborators = async ({ resource, resourceType, diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 9e5bcef071ab..59bbeff5b192 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -1,5 +1,5 @@ import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { getResourcePermission, parseHeaderCert } from '../controller'; +import { getResourcePermission } from '../controller'; import { type CollectionWithDatasetType, type DatasetDataItemType, @@ -21,6 +21,7 @@ import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { DataSetDefaultRoleVal } from '@fastgpt/global/support/permission/dataset/constant'; import { getDatasetImagePreviewUrl } from '../../../core/dataset/image/utils'; import { i18nT } from '../../../../web/i18n/utils'; +import { parseHeaderCert } from '../auth/common'; export const authDatasetByTmbId = async ({ tmbId, diff --git a/packages/service/support/permission/memberGroup/controllers.ts b/packages/service/support/permission/memberGroup/controllers.ts index 3d775e11c1be..3620a8b24f1d 100644 --- a/packages/service/support/permission/memberGroup/controllers.ts +++ b/packages/service/support/permission/memberGroup/controllers.ts @@ -1,6 +1,5 @@ import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; import { MongoGroupMemberModel } from './groupMemberSchema'; -import { parseHeaderCert } from '../controller'; import { MongoMemberGroupModel } from './memberGroupSchema'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; import { type ClientSession } from 'mongoose'; @@ -9,6 +8,7 @@ import { type AuthModeType, type AuthResponseType } from '../type'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; +import { parseHeaderCert } from '../auth/common'; /** * Get the default group of a team diff --git a/packages/service/support/permission/publish/authLink.ts b/packages/service/support/permission/publish/authLink.ts index 269ce4854aa0..479c3babbc47 100644 --- a/packages/service/support/permission/publish/authLink.ts +++ b/packages/service/support/permission/publish/authLink.ts @@ -1,11 +1,11 @@ import { type AppDetailType } from '@fastgpt/global/core/app/type'; import { type OutlinkAppType, type OutLinkSchema } from '@fastgpt/global/support/outLink/type'; -import { parseHeaderCert } from '../controller'; import { MongoOutLink } from '../../outLink/schema'; import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink'; import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant'; import { authAppByTmbId } from '../app/auth'; import { type AuthModeType, type AuthResponseType } from '../type'; +import { parseHeaderCert } from '../auth/common'; /* crud outlink permission */ export async function authOutLinkCrud({ diff --git a/packages/service/support/permission/user/auth.ts b/packages/service/support/permission/user/auth.ts index 5029008fc13a..88dd38555991 100644 --- a/packages/service/support/permission/user/auth.ts +++ b/packages/service/support/permission/user/auth.ts @@ -1,11 +1,10 @@ import { type TeamTmbItemType } from '@fastgpt/global/support/user/team/type'; -import { parseHeaderCert } from '../controller'; import { getTmbInfoByTmbId } from '../../user/team/controller'; import { TeamErrEnum } from '@fastgpt/global/common/error/code/team'; import { type AuthModeType, type AuthResponseType } from '../type'; import { NullPermissionVal } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; -import { authCert } from '../auth/common'; +import { authCert, parseHeaderCert } from '../auth/common'; import { MongoUser } from '../../user/schema'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { type ApiRequestProps } from '../../../type/next'; diff --git a/projects/app/src/pages/api/common/file/read.ts b/projects/app/src/pages/api/common/file/read.ts index 0f84b5e955a0..c81a4b0def0b 100644 --- a/projects/app/src/pages/api/common/file/read.ts +++ b/projects/app/src/pages/api/common/file/read.ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/read/[filename].ts b/projects/app/src/pages/api/common/file/read/[filename].ts index ba715fbe9398..fbd9d20b5879 100644 --- a/projects/app/src/pages/api/common/file/read/[filename].ts +++ b/projects/app/src/pages/api/common/file/read/[filename].ts @@ -1,9 +1,9 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { jsonRes } from '@fastgpt/service/common/response'; -import { authFileToken } from '@fastgpt/service/support/permission/controller'; import { getDownloadStream, getFileById } from '@fastgpt/service/common/file/gridfs/controller'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { stream2Encoding } from '@fastgpt/service/common/file/gridfs/utils'; +import { authFileToken } from '@fastgpt/service/support/permission/auth/file'; const previewableExtensions = [ 'jpg', diff --git a/projects/app/src/pages/api/common/file/upload.ts b/projects/app/src/pages/api/common/file/upload.ts index 63340f78488b..e0845a2b92b3 100644 --- a/projects/app/src/pages/api/common/file/upload.ts +++ b/projects/app/src/pages/api/common/file/upload.ts @@ -4,7 +4,6 @@ import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller'; import { getUploadModel } from '@fastgpt/service/common/file/multer'; import { removeFilesByPaths } from '@fastgpt/service/common/file/utils'; import { NextAPI } from '@/service/middleware/entry'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { addLog } from '@fastgpt/service/common/system/log'; import { authFrequencyLimit } from '@/service/common/frequencyLimit/api'; @@ -13,6 +12,7 @@ import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authDataset } from '@fastgpt/service/support/permission/dataset/auth'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type UploadChatFileProps = { appId: string; diff --git a/projects/app/src/pages/api/core/dataset/collection/read.ts b/projects/app/src/pages/api/core/dataset/collection/read.ts index 19935b3c2c2f..ed510848a04f 100644 --- a/projects/app/src/pages/api/core/dataset/collection/read.ts +++ b/projects/app/src/pages/api/core/dataset/collection/read.ts @@ -2,7 +2,6 @@ import type { ApiRequestProps } from '@fastgpt/service/type/next'; import { NextAPI } from '@/service/middleware/entry'; import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth'; import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constants'; -import { createFileToken } from '@fastgpt/service/support/permission/controller'; import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; @@ -10,6 +9,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset'; import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat'; import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller'; import { getApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset'; +import { createFileToken } from '@fastgpt/service/support/permission/auth/file'; export type readCollectionSourceQuery = {}; @@ -48,7 +48,7 @@ async function handler( }); } - /* + /* 1. auth chat read permission 2. auth collection quote in chat 3. auth outlink open show quote diff --git a/projects/app/src/pages/api/support/user/account/loginByPassword.ts b/projects/app/src/pages/api/support/user/account/loginByPassword.ts index deaa82583435..d2b11d9faf53 100644 --- a/projects/app/src/pages/api/support/user/account/loginByPassword.ts +++ b/projects/app/src/pages/api/support/user/account/loginByPassword.ts @@ -1,6 +1,5 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import { MongoUser } from '@fastgpt/service/support/user/schema'; -import { setCookie } from '@fastgpt/service/support/permission/controller'; import { getUserDetail } from '@fastgpt/service/support/user/controller'; import type { PostLoginProps } from '@fastgpt/global/support/user/api.d'; import { UserStatusEnum } from '@fastgpt/global/support/user/constant'; @@ -15,6 +14,7 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants'; import { authCode } from '@fastgpt/service/support/user/auth/controller'; import { createUserSession } from '@fastgpt/service/support/user/session'; import requestIp from 'request-ip'; +import { setCookie } from '@fastgpt/service/support/permission/auth/common'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { username, password, code } = req.body as PostLoginProps; diff --git a/projects/app/src/pages/api/support/user/account/loginout.ts b/projects/app/src/pages/api/support/user/account/loginout.ts index 423c5a49bde9..5e86d3c66ba1 100644 --- a/projects/app/src/pages/api/support/user/account/loginout.ts +++ b/projects/app/src/pages/api/support/user/account/loginout.ts @@ -1,7 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; -import { clearCookie } from '@fastgpt/service/support/permission/controller'; import { NextAPI } from '@/service/middleware/entry'; -import { authCert } from '@fastgpt/service/support/permission/auth/common'; +import { authCert, clearCookie } from '@fastgpt/service/support/permission/auth/common'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; async function handler(req: NextApiRequest, res: NextApiResponse) { diff --git a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts index 19052a30286c..f0b1da3f7d91 100644 --- a/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts +++ b/projects/app/src/pages/api/support/user/account/updatePasswordByOld.ts @@ -8,7 +8,6 @@ import { NextAPI } from '@/service/middleware/entry'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { delUserAllSession } from '@fastgpt/service/support/user/session'; -import { parseHeaderCert } from '@fastgpt/service/support/permission/controller'; async function handler(req: NextApiRequest, res: NextApiResponse) { const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string }; From 5f7f2528b239e40bcbd90737be8aa1909a60ac4b Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Thu, 4 Sep 2025 20:23:33 +0800 Subject: [PATCH 15/17] refactor(permission): remove selfPermission --- .../support/permission/collaborator.d.ts | 1 - packages/global/support/permission/type.ts | 1 - packages/global/support/permission/utils.ts | 75 ++++++- packages/service/common/mongo/index.ts | 21 +- .../service/support/permission/app/auth.ts | 9 +- .../service/support/permission/controller.ts | 208 +++++++++++------- .../support/permission/dataset/auth.ts | 8 +- .../support/permission/inheritPermission.ts | 189 +++++++--------- .../support/permission/org/orgSchema.ts | 8 +- packages/service/support/permission/schema.ts | 3 - .../service/support/user/team/controller.ts | 4 +- projects/app/src/pages/api/admin/initv4130.ts | 118 ---------- projects/app/src/pages/api/core/app/update.ts | 11 +- .../app/src/pages/api/core/dataset/update.ts | 11 +- 14 files changed, 320 insertions(+), 347 deletions(-) delete mode 100644 projects/app/src/pages/api/admin/initv4130.ts diff --git a/packages/global/support/permission/collaborator.d.ts b/packages/global/support/permission/collaborator.d.ts index 8a873436dbcb..6e927c8122e3 100644 --- a/packages/global/support/permission/collaborator.d.ts +++ b/packages/global/support/permission/collaborator.d.ts @@ -13,7 +13,6 @@ export type CollaboratorIdType = RequireOnlyOne<{ export type CollaboratorItemDetailType = { teamId: string; permission: Permission; - selfPermission?: Permission; name: string; avatar: string; } & CollaboratorIdType; diff --git a/packages/global/support/permission/type.ts b/packages/global/support/permission/type.ts index 0c3df5cc3d7a..5b77958d9a65 100644 --- a/packages/global/support/permission/type.ts +++ b/packages/global/support/permission/type.ts @@ -62,7 +62,6 @@ export type ResourcePermissionType = { teamId: string; resourceType: ResourceType; permission: PermissionValueType; - selfPermission?: PermissionValueType; resourceId: string; } & RequireOnlyOne<{ tmbId: string; diff --git a/packages/global/support/permission/utils.ts b/packages/global/support/permission/utils.ts index abde7f43389e..3e2012f98192 100644 --- a/packages/global/support/permission/utils.ts +++ b/packages/global/support/permission/utils.ts @@ -1,4 +1,5 @@ import type { CollaboratorIdType, CollaboratorItemType } from './collaborator'; +import type { RoleValueType} from './type'; import { type PermissionValueType } from './type'; /** * Sum the permission value. @@ -53,15 +54,25 @@ export const checkRoleUpdateConflict = ({ return false; }; +export type ChangedClbType = { + changedRole: RoleValueType; + deleted: boolean; +} & CollaboratorIdType; + /** * Get changed collaborators. * return empty array if all collaborators are unchanged. - * for each return item: { - * id: string; // collaborator id + * + * for each return item: + * ```typescript + * { + * // ... ids * changedRole: number; // set bit means the role is changed + * deleted: boolean; // is deleted * } - * special: for low 3 bit: always get the lowest change, unset the higher change. - * @param param0 + * ``` + * + * **special**: for low 3 bit: always get the lowest change, unset the higher change. */ export const getChangedCollaborators = ({ oldRealClbs, @@ -69,21 +80,23 @@ export const getChangedCollaborators = ({ }: { oldRealClbs: CollaboratorItemType[]; newRealClbs: CollaboratorItemType[]; -}) => { +}): ChangedClbType[] => { if (oldRealClbs.length === 0) { return newRealClbs.map((clb) => ({ ...clb, - changedRole: clb.permission + changedRole: clb.permission, + deleted: false })); } const oldClbsMap = new Map(oldRealClbs.map((clb) => [getCollaboratorId(clb), clb])); - const changedClbs = []; + const changedClbs: ChangedClbType[] = []; for (const newClb of newRealClbs) { const oldClb = oldClbsMap.get(getCollaboratorId(newClb)); if (!oldClb) { changedClbs.push({ ...newClb, - changedRole: newClb.permission + changedRole: newClb.permission, + deleted: false }); continue; } @@ -91,7 +104,8 @@ export const getChangedCollaborators = ({ if (changedRole) { changedClbs.push({ ...newClb, - changedRole + changedRole, + deleted: false }); } } @@ -119,3 +133,46 @@ export const getChangedCollaborators = ({ export const getCollaboratorId = (clb: CollaboratorIdType) => (clb.tmbId || clb.groupId || clb.orgId)!; + +/** + * merge collaboratorLists into one list + * the intersection of the lists will calculate the sumPer. + */ +export const mergeCollaboratorList = (...clbLists: T[][]): T[] => { + const merge = (list1: T[], list2: T[]): T[] => { + const idToClb = new Map(); + + // Add all items from list1 + for (const clb of list1) { + idToClb.set(getCollaboratorId(clb), { ...clb }); + } + + // Merge permissions from list2 + for (const clb2 of list2) { + const id = getCollaboratorId(clb2); + if (idToClb.has(id)) { + // If already exists, merge permission bits + const original = idToClb.get(id)!; + idToClb.set(id, { + ...original, + permission: sumPer(original.permission, clb2.permission)! + }); + } else { + idToClb.set(id, { ...clb2 }); + } + } + + return Array.from(idToClb.values()); + }; + if (clbLists.length === 0) { + return []; + } + if (clbLists.length === 1) { + return clbLists[0]; + } + if (clbLists.length === 2) { + const [list1, list2] = clbLists; + return merge(list1, list2); + } + return mergeCollaboratorList(merge(clbLists[0], clbLists[1]), ...clbLists.slice(2)); +}; diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 5666d6057464..6e19f9144eac 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -72,17 +72,18 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { const convertObjectIds = (obj: any) => { if (!obj) return; - // Convert _id - if (obj._id && obj._id.toString) { - obj._id = obj._id.toString(); - } - - // Convert other ObjectId fields - Object.keys(obj).forEach((key) => { - if (obj[key] && obj[key]._bsontype === 'ObjectId') { - obj[key] = obj[key].toString(); + // Convert _id and other ObjectId fields in one pass + for (const key in obj) { + const val = obj[key]; + if ( + val && + typeof val === 'object' && + val._bsontype === 'ObjectId' && + typeof val.toString === 'function' + ) { + obj[key] = val.toString(); } - }); + } }; if (Array.isArray(docs)) { diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 38098642da38..9f45bd483cc0 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -8,7 +8,7 @@ import { } from '@fastgpt/global/support/permission/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; import { getTmbInfoByTmbId } from '../../user/team/controller'; -import { getResourcePermission } from '../controller'; +import { getTmbPermission } from '../controller'; import { AppPermission } from '@fastgpt/global/support/permission/app/controller'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; @@ -90,11 +90,14 @@ export const authAppByTmbId = async ({ const isOwner = tmbPer.isOwner || String(app.tmbId) === String(tmbId); + const isGetParentClb = + app.inheritPermission && !AppFolderTypeList.includes(app.type) && !!app.parentId; + const Per = new AppPermission({ - role: await getResourcePermission({ + role: await getTmbPermission({ teamId, tmbId, - resourceId: appId, + resourceId: isGetParentClb ? app.parentId! : app._id, resourceType: PerResourceTypeEnum.app }), isOwner diff --git a/packages/service/support/permission/controller.ts b/packages/service/support/permission/controller.ts index 293ce9e1982b..880a3b0119c3 100644 --- a/packages/service/support/permission/controller.ts +++ b/packages/service/support/permission/controller.ts @@ -1,26 +1,28 @@ import type { ClientSession, AnyBulkWriteOperation } from '../../common/mongo'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; -import { - ManageRoleVal, - NullRoleVal, - OwnerRoleVal -} from '@fastgpt/global/support/permission/constant'; +import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant'; import { MongoResourcePermission } from './schema'; -import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; +import type { ResourcePermissionType, ResourceType } from '@fastgpt/global/support/permission/type'; import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; import { getGroupsByTmbId } from './memberGroup/controllers'; import { Permission } from '@fastgpt/global/support/permission/controller'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; -import { type MemberGroupSchemaType } from '@fastgpt/global/support/permission/memberGroup/type'; -import { type TeamMemberSchema } from '@fastgpt/global/support/user/team/type'; -import { type OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { getOrgIdSetWithParentByTmbId } from './org/controllers'; -import { sumPer } from '@fastgpt/global/support/permission/utils'; -import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; +import { + getCollaboratorId, + mergeCollaboratorList, + sumPer +} from '@fastgpt/global/support/permission/utils'; import { type SyncChildrenPermissionResourceType } from './inheritPermission'; import { pickCollaboratorIdFields } from './utils'; -import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; +import type { + CollaboratorItemDetailType, + CollaboratorItemType +} from '@fastgpt/global/support/permission/collaborator'; +import { MongoTeamMember } from '../../support/user/team/teamMemberSchema'; +import { MongoOrgModel } from './org/orgSchema'; +import { MongoMemberGroupModel } from './memberGroup/memberGroupSchema'; /** get resource permission for a team member * If there is no permission for the team member, it will return undefined @@ -30,7 +32,7 @@ import type { CollaboratorItemType } from '@fastgpt/global/support/permission/co * @param resourceId * @returns PermissionValueType | undefined */ -export const getResourcePermission = async ({ +export const getTmbPermission = async ({ resourceType, teamId, tmbId, @@ -105,7 +107,10 @@ export const getResourcePermission = async ({ return sumPer(...groupPers, ...orgPers); }; -export async function getResourceClbs({ +/** + * Only get resource's owned clbs, not including parents'. + */ +export async function getResourceOwnedClbs({ resourceType, teamId, resourceId, @@ -134,14 +139,105 @@ export async function getResourceClbs({ ).lean(); } +export const getClbsInfo = async ({ + clbs, + teamId, + ownerTmbId +}: { + clbs: CollaboratorItemType[]; + teamId: string; + ownerTmbId?: string; +}): Promise => { + const tmbIds = []; + const orgIds = []; + const groupIds = []; + + for (const clb of clbs) { + if (clb.tmbId) tmbIds.push(clb.tmbId); + if (clb.orgId) orgIds.push(clb.orgId); + if (clb.groupId) groupIds.push(clb.groupId); + } + + const infos = ( + await Promise.all([ + MongoTeamMember.find({ _id: { $in: tmbIds }, teamId }, '_id name avatar').lean(), + MongoOrgModel.find({ _id: { $in: orgIds }, teamId }, '_id name avatar').lean(), + MongoMemberGroupModel.find({ _id: { $in: groupIds }, teamId }, '_id name avatar').lean() + ]) + ).flat(); + + return clbs.map((clb) => { + const info = infos.find((info) => info._id === getCollaboratorId(clb))!; + return { + ...clb, + teamId, + permission: new Permission({ role: clb.permission, isOwner: ownerTmbId === clb.tmbId }), + name: info.name, + avatar: info.avatar + }; + }); +}; + +/** + * Get Resource's Collaborators (owned and parent's if it is inherit) + */ +export const getResourceClbs = async ({ + resourceType, + teamId, + resourceId, + session, + inherit, + isFolder, + parentId +}: { + teamId: string; + session?: ClientSession; + parentId: ParentIdType; + inherit?: boolean; + isFolder?: boolean; +} & ( + | { + resourceId: ParentIdType; + resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>; + } + | { + resourceId: ParentIdType; + resourceType: 'team'; + } +)) => { + const ownedClbs = await getResourceOwnedClbs({ + resourceType, + resourceId, + teamId, + session + }); + if (inherit === false || isFolder || !parentId || resourceType === 'team') { + return ownedClbs; + } + // otherwise, we need the parent's clbs + const parentClbs = await getResourceOwnedClbs({ + resourceType, + resourceId, + teamId + }); + + return mergeCollaboratorList(parentClbs, ownedClbs); +}; + export const getClbsWithInfo = async ({ resourceId, resourceType, teamId, - tmbId + tmbId, + parentId, + isFolder, + inherit }: { teamId: string; tmbId?: string; + parentId?: string; + isFolder?: boolean; + inherit?: boolean; } & ( | { resourceId: ParentIdType; @@ -155,67 +251,21 @@ export const getClbsWithInfo = async ({ if (!resourceId && resourceType !== 'team') { return []; } - return Promise.all([ - ...( - await MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - tmbId: { - $exists: true - } - }) - .populate<{ tmb: TeamMemberSchema }>({ - path: 'tmb', - select: 'name userId avatar' - }) - .lean() - ) - .map((item) => ({ - tmbId: item.tmb._id, - teamId: item.teamId, - permission: new Permission({ role: item.permission, isOwner: item.tmbId === tmbId }), - name: item.tmb.name, - avatar: item.tmb.avatar - })) - .sort((a, b) => a.name.localeCompare(b.name)), - ...( - await MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - groupId: { - $exists: true - } - }) - .populate<{ group: MemberGroupSchemaType }>('group', 'name avatar') - .lean() - ).map((item) => ({ - groupId: item.group._id, - teamId: item.teamId, - permission: new Permission({ role: item.permission }), - name: item.group.name, - avatar: item.group.avatar - })), - ...( - await MongoResourcePermission.find({ - teamId, - resourceId, - resourceType, - orgId: { - $exists: true - } - }) - .populate<{ org: OrgSchemaType }>({ path: 'org', select: 'name avatar' }) - .lean() - ).map((item) => ({ - orgId: item.org._id, - teamId: item.teamId, - permission: new Permission({ role: item.permission }), - name: item.org.name, - avatar: item.org.avatar || DEFAULT_ORG_AVATAR - })) - ]); + + const clbs = await getResourceClbs({ + resourceType, + resourceId, + teamId, + inherit, + isFolder, + parentId + }); + + return getClbsInfo({ + clbs, + ownerTmbId: tmbId, + teamId + }); }; export const delResourcePermissionById = (id: string) => { @@ -265,7 +315,7 @@ export const createResourceDefaultCollaborators = async ({ session: ClientSession; tmbId: string; }) => { - const parentClbs = await getResourceClbs({ + const parentClbs = await getResourceOwnedClbs({ resourceId: resource.parentId, resourceType, teamId: resource.teamId, @@ -280,7 +330,6 @@ export const createResourceDefaultCollaborators = async ({ .map((clb) => { if (clb.permission === OwnerRoleVal) { clb.permission = ManageRoleVal; - clb.selfPermission = NullRoleVal; } return clb; }), @@ -303,8 +352,7 @@ export const createResourceDefaultCollaborators = async ({ }, update: { $set: { - permission: clb.permission, - selfPermission: NullRoleVal + permission: clb.permission } }, upsert: true diff --git a/packages/service/support/permission/dataset/auth.ts b/packages/service/support/permission/dataset/auth.ts index 59bbeff5b192..2b4d20d54126 100644 --- a/packages/service/support/permission/dataset/auth.ts +++ b/packages/service/support/permission/dataset/auth.ts @@ -1,5 +1,5 @@ import { type PermissionValueType } from '@fastgpt/global/support/permission/type'; -import { getResourcePermission } from '../controller'; +import { getTmbPermission } from '../controller'; import { type CollectionWithDatasetType, type DatasetDataItemType, @@ -62,13 +62,15 @@ export const authDatasetByTmbId = async ({ } const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId); + const isGetParentClb = + dataset.inheritPermission && dataset.type !== DatasetTypeEnum.folder && !!dataset.parentId; // get dataset permission or inherit permission from parent folder. const Per = new DatasetPermission({ - role: await getResourcePermission({ + role: await getTmbPermission({ teamId, tmbId, - resourceId: datasetId, + resourceId: isGetParentClb ? dataset.parentId! : datasetId, resourceType: PerResourceTypeEnum.dataset }), isOwner diff --git a/packages/service/support/permission/inheritPermission.ts b/packages/service/support/permission/inheritPermission.ts index a7345e0e8aa3..c8c946818e8e 100644 --- a/packages/service/support/permission/inheritPermission.ts +++ b/packages/service/support/permission/inheritPermission.ts @@ -7,10 +7,15 @@ import { } from '@fastgpt/global/support/permission/constant'; import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type'; import { mongoSessionRun } from '../../common/mongo/sessionRun'; -import { getResourceClbs } from './controller'; +import { getResourceOwnedClbs } from './controller'; import { MongoResourcePermission } from './schema'; import type { ClientSession, Model, AnyBulkWriteOperation } from '../../common/mongo'; -import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils'; +import { + getChangedCollaborators, + getCollaboratorId, + mergeCollaboratorList, + sumPer +} from '@fastgpt/global/support/permission/utils'; import type { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator'; import { pickCollaboratorIdFields } from './utils'; @@ -44,7 +49,7 @@ export async function syncChildrenPermission({ // should be provided when inheritPermission is true session: ClientSession; - collaborators?: CollaboratorItemType[]; + collaborators: CollaboratorItemType[]; }) { // only folder has permission const isFolder = folderTypeList.includes(resource.type); @@ -54,11 +59,14 @@ export async function syncChildrenPermission({ if (!isFolder) return; // get all the resource permission of the app - const allResources = await resourceModel + const allFolders = await resourceModel .find( { teamId, - inheritPermission: true + inheritPermission: true, + type: { + $in: folderTypeList + } }, '_id parentId' ) @@ -67,7 +75,10 @@ export async function syncChildrenPermission({ const allClbs = await MongoResourcePermission.find({ resourceType, - teamId + teamId, + resourceId: { + $in: allFolders.map((folder) => folder._id) + } }) .lean() .session(session); @@ -78,7 +89,7 @@ export async function syncChildrenPermission({ const parentChildrenMap = new Map(); // init the map - allResources.forEach((resource) => { + allFolders.forEach((resource) => { resourceMap.set(resource._id, resource); const parentId = String(resource.parentId); if (!parentChildrenMap.has(parentId)) { @@ -118,6 +129,7 @@ export async function syncChildrenPermission({ // BFS to get all children const queue = [String(resource._id)]; const ops: AnyBulkWriteOperation[] = []; + const clbMap = new Map(collaborators.map((clb) => [getCollaboratorId(clb), { ...clb }])); while (queue.length) { const parentId = String(queue.shift()); @@ -126,96 +138,58 @@ export async function syncChildrenPermission({ for (const child of _children) { // 1. get parent's permission and what permission I have. const parentClbs = resourceIdClbMap.get(String(child.parentId)) || []; - const myClbs = resourceIdClbMap.get(String(child._id)) || []; - // 2. merge the permission and generate operations for mongo. - // rules: - // i. if parent has and I have not, get the clb. - // ii. if parent has not and I have. If my clb has no selfPermission, - // it should be removed. Otherwise, it should be remained. - // iii. if we both have, get the sumPer. - - const bothHaveClbs = parentClbs - .filter((clb) => - myClbs.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) - ) - .map((clb) => ({ - ...clb, - permission: sumPer( - clb.permission, - myClbs.find((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb))!.permission - )! - })); - - const parentHasAndIHaveNot = parentClbs.filter( - (clb) => !myClbs.some((myClb) => getCollaboratorId(clb) === getCollaboratorId(myClb)) - ); + const myClbs = resourceIdClbMap.get(child._id) || []; + const parentIdSet = new Set(parentClbs.map((clb) => getCollaboratorId(clb))); + const myClbsIdSet = new Set(myClbs.map((clb) => getCollaboratorId(clb))); - const IHaveAndParentHasNot = myClbs.filter( - (clb) => - !parentClbs.some((parentClb) => getCollaboratorId(clb) === getCollaboratorId(parentClb)) - ); - - // generate ops - // i. - for (const clb of parentHasAndIHaveNot) { - ops.push({ - updateOne: { - filter: { - resourceId: child._id, - resourceType, - teamId, - ...pickCollaboratorIdFields(clb) - }, - update: { - permission: clb.permission, - selfPermission: NullPermissionVal - }, - upsert: true - } - }); - } - - // ii. - for (const clb of IHaveAndParentHasNot) { - if (clb.selfPermission === NullPermissionVal) { + // add or update + for (const clb of collaborators) { + if (!myClbsIdSet.has(getCollaboratorId(clb))) { ops.push({ - deleteOne: { - filter: { + insertOne: { + document: { resourceId: child._id, resourceType, teamId, + permission: parentClbs[0].permission, ...pickCollaboratorIdFields(clb) + } as ResourcePermissionType + } + }); + } else { + const myclb = myClbs.find((clb) => getCollaboratorId(clb) === getCollaboratorId(clb))!; + ops.push({ + updateOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(clb), + resourceType + }, + update: { + permission: sumPer(myclb.permission, clb.permission) } } }); } } - // iii. - for (const clb of bothHaveClbs) { - ops.push({ - updateOne: { - filter: { - resourceId: child._id, - resourceType, - teamId, - ...pickCollaboratorIdFields(clb) - }, - update: { - permission: clb.permission - // do not update selfPermission - }, - upsert: true - } - }); + // delele + for (const myClb of myClbs) { + if (!clbMap.get(getCollaboratorId(myClb))) { + // the new collaborators doesnt have it. remove it + ops.push({ + deleteOne: { + filter: { + resourceId: child._id, + teamId, + ...pickCollaboratorIdFields(myClb), + resourceType + } + } + }); + } } - // 3. save the permission status for my children - resourceIdClbMap.set(child._id, [ - ...parentHasAndIHaveNot, - ...bothHaveClbs, - ...IHaveAndParentHasNot.filter((clb) => clb.selfPermission !== NullPermissionVal) - ]); - // 4. add myself to the queue queue.push(child._id); } } @@ -240,6 +214,7 @@ export async function resumeInheritPermission({ resourceModel: typeof Model; session?: ClientSession; }) { + const isFolder = folderTypeList.includes(resource.type); const fn = async (session: ClientSession) => { // update the resource permission await resourceModel.updateOne( @@ -253,32 +228,32 @@ export async function resumeInheritPermission({ ); // Folder resource, need to sync children - const parentClbs = await getResourceClbs({ + const parentClbs = await getResourceOwnedClbs({ resourceId: resource.parentId, teamId: resource.teamId, resourceType, session }); - // sync self - await syncCollaborators({ - resourceType, - collaborators: parentClbs, - teamId: resource.teamId, - resourceId: resource._id, - session - }); - // sync children - await syncChildrenPermission({ - resource, - resourceModel, - folderTypeList, - resourceType, - session, - collaborators: parentClbs - }); - // Not folder, delete all clb - // await MongoResourcePermission.deleteMany({ resourceId: resource._id }, { session }); + if (isFolder) { + // sync self + await syncCollaborators({ + resourceType, + collaborators: parentClbs, + teamId: resource.teamId, + resourceId: resource._id, + session + }); + // sync children + await syncChildrenPermission({ + resource, + resourceModel, + folderTypeList, + resourceType, + session, + collaborators: parentClbs + }); + } }; if (session) { @@ -331,8 +306,7 @@ export async function syncCollaborators({ ...pickCollaboratorIdFields(clb) }, update: { - permission, - selfPermission: clb.permission + permission } } }); @@ -350,8 +324,7 @@ export async function syncCollaborators({ resourceId, resourceType, ...pickCollaboratorIdFields(clb), - permission: clb.permission, - selfPermission: NullPermissionVal + permission: clb.permission } as ResourcePermissionType } }); diff --git a/packages/service/support/permission/org/orgSchema.ts b/packages/service/support/permission/org/orgSchema.ts index 94fa531e466f..a695ffb1dd9d 100644 --- a/packages/service/support/permission/org/orgSchema.ts +++ b/packages/service/support/permission/org/orgSchema.ts @@ -4,6 +4,7 @@ import type { OrgSchemaType } from '@fastgpt/global/support/user/team/org/type'; import { connectionMongo, getMongoModel } from '../../../common/mongo'; import { OrgMemberCollectionName } from './orgMemberSchema'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { DEFAULT_ORG_AVATAR } from '@fastgpt/global/common/system/constants'; const { Schema } = connectionMongo; export const OrgSchema = new Schema( @@ -29,7 +30,12 @@ export const OrgSchema = new Schema( type: String, required: true }, - avatar: String, + avatar: { + type: String, + get: function (value: string) { + return value || DEFAULT_ORG_AVATAR; + } + }, description: String, updateTime: { type: Date, diff --git a/packages/service/support/permission/schema.ts b/packages/service/support/permission/schema.ts index 388482b66fcd..56c5e602d85c 100644 --- a/packages/service/support/permission/schema.ts +++ b/packages/service/support/permission/schema.ts @@ -45,9 +45,6 @@ export const ResourcePermissionSchema = new Schema({ * Optional. Only be set when the resource is *inherited* from the parent resource. * For recording the self permission. When cancel the inheritance, it will overwrite the permission property and set to `unset`. */ - selfPermission: { - type: Number - }, // Resource ID: App or DataSet or any other resource type. // It is null if the resourceType is team. resourceId: { diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index a399bd35884d..fa8ed440b390 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -8,7 +8,7 @@ import { import { MongoTeamMember } from './teamMemberSchema'; import { MongoTeam } from './teamSchema'; import { type UpdateTeamProps } from '@fastgpt/global/support/user/team/controller'; -import { getResourcePermission } from '../../permission/controller'; +import { getTmbPermission } from '../../permission/controller'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { TeamPermission } from '@fastgpt/global/support/permission/user/controller'; import { TeamDefaultRoleVal } from '@fastgpt/global/support/permission/user/constant'; @@ -26,7 +26,7 @@ async function getTeamMember(match: Record): Promise, - res: ApiResponseType -) { - await authCert({ req, authRoot: true }); - // 1. update App's - const appFolders = await MongoApp.find({ - $or: [ - { - parentId: null, - type: { $in: AppFolderTypeList } - }, - { - parentId: { $exists: true }, - inheritPermission: false, - type: { $in: AppFolderTypeList } - } - ] - }).lean(); - addLog.info(`start sync app children permission, total: ${appFolders.length}`); - for (const folder of appFolders) { - await mongoSessionRun(async (session) => { - const clbs = await getResourceClbs({ - resourceType: PerResourceTypeEnum.app, - resourceId: folder._id, - teamId: folder.teamId, - session - }); - await syncChildrenPermission({ - folderTypeList: AppFolderTypeList, - resource: folder, - session, - resourceModel: MongoApp, - resourceType: PerResourceTypeEnum.app, - collaborators: clbs - }); - }); - addLog.debug( - `sync app children permission, folderId: ${folder._id}, progress: ${appFolders.indexOf(folder) + 1}/${appFolders.length}` - ); - } - addLog.info('sync app children permission completed'); - - // 2. update Dataset's - const datasetFolders = await MongoDataset.find({ - $or: [ - { - parentId: null, - type: DatasetTypeEnum.folder - }, - { - parentId: { $exists: true }, - inheritPermission: false, - type: DatasetTypeEnum.folder - } - ] - }).lean(); - - addLog.info(`start sync dataset children permission, total: ${datasetFolders.length}`); - - for (const folder of datasetFolders) { - await mongoSessionRun(async (session) => { - const clbs = await getResourceClbs({ - resourceType: PerResourceTypeEnum.dataset, - resourceId: folder._id, - teamId: folder.teamId, - session - }); - - await syncChildrenPermission({ - folderTypeList: [DatasetTypeEnum.folder], - resource: folder, - session, - resourceModel: MongoDataset, - resourceType: PerResourceTypeEnum.dataset, - collaborators: clbs - }); - }); - - addLog.debug( - `sync dataset children permission, folderId: ${folder._id}, progress: ${datasetFolders.indexOf(folder) + 1}/${datasetFolders.length}` - ); - } - - addLog.info('sync dataset children permission completed'); - - return { - message: 'App and Dataset permission sync completed successfully' - }; -} - -export default NextAPI(handler); diff --git a/projects/app/src/pages/api/core/app/update.ts b/projects/app/src/pages/api/core/app/update.ts index 02a67af0875c..7be66b30dd93 100644 --- a/projects/app/src/pages/api/core/app/update.ts +++ b/projects/app/src/pages/api/core/app/update.ts @@ -18,7 +18,10 @@ import { import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ClientSession } from 'mongoose'; import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun'; -import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; +import { + getResourceClbs, + getResourceOwnedClbs +} from '@fastgpt/service/support/permission/controller'; import { authUserPer } from '@fastgpt/service/support/permission/user/auth'; import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant'; import { AppErrEnum } from '@fastgpt/global/common/error/code/app'; @@ -150,7 +153,7 @@ async function handler(req: ApiRequestProps) { if (isMove) { await mongoSessionRun(async (session) => { // Inherit folder: Sync children permission and it's clbs - const parentClbsAndGroups = await getResourceClbs({ + const parentClbs = await getResourceOwnedClbs({ teamId: app.teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.app, @@ -160,7 +163,7 @@ async function handler(req: ApiRequestProps) { await syncCollaborators({ resourceId: app._id, resourceType: PerResourceTypeEnum.app, - collaborators: parentClbsAndGroups, + collaborators: parentClbs, session, teamId: app.teamId }); @@ -170,7 +173,7 @@ async function handler(req: ApiRequestProps) { resourceType: PerResourceTypeEnum.app, resourceModel: MongoApp, folderTypeList: AppFolderTypeList, - collaborators: parentClbsAndGroups, + collaborators: parentClbs, session }); logAppMove({ tmbId, teamId, app, targetName }); diff --git a/projects/app/src/pages/api/core/dataset/update.ts b/projects/app/src/pages/api/core/dataset/update.ts index 024a73628dc9..25eba406d1e7 100644 --- a/projects/app/src/pages/api/core/dataset/update.ts +++ b/projects/app/src/pages/api/core/dataset/update.ts @@ -38,7 +38,10 @@ import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util'; import { getEmbeddingModel, getLLMModel } from '@fastgpt/service/core/ai/model'; import { computedCollectionChunkSettings } from '@fastgpt/global/core/dataset/training/utils'; -import { getResourceClbs } from '@fastgpt/service/support/permission/controller'; +import { + getResourceClbs, + getResourceOwnedClbs +} from '@fastgpt/service/support/permission/controller'; export type DatasetUpdateQuery = {}; export type DatasetUpdateResponse = any; @@ -230,7 +233,7 @@ async function handler( await mongoSessionRun(async (session) => { if (isMove) { - const parentClbsAndGroups = await getResourceClbs({ + const parentClbs = await getResourceOwnedClbs({ teamId: dataset.teamId, resourceId: parentId, resourceType: PerResourceTypeEnum.dataset, @@ -241,7 +244,7 @@ async function handler( teamId: dataset.teamId, resourceId: id, resourceType: PerResourceTypeEnum.dataset, - collaborators: parentClbsAndGroups, + collaborators: parentClbs, session }); @@ -250,7 +253,7 @@ async function handler( resourceType: PerResourceTypeEnum.dataset, resourceModel: MongoDataset, folderTypeList: [DatasetTypeEnum.folder], - collaborators: parentClbsAndGroups, + collaborators: parentClbs, session }); logDatasetMove({ tmbId, teamId, dataset, targetName }); From b44bc66163021b7a044c2f0259506ba1ef77b6fa Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Thu, 4 Sep 2025 20:50:03 +0800 Subject: [PATCH 16/17] fix: mock --- test/mocks/request.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/mocks/request.ts b/test/mocks/request.ts index 564b2e3d6d26..a129c9d50ad8 100644 --- a/test/mocks/request.ts +++ b/test/mocks/request.ts @@ -56,6 +56,7 @@ export type parseHeaderCertRet = { sourceName: string | undefined; apikey: string; isRoot: boolean; + sessionId: string; }; export type MockReqType = { @@ -66,7 +67,7 @@ export type MockReqType = { [key: string]: any; }; -vi.mock(import('@fastgpt/service/support/permission/controller'), async (importOriginal) => { +vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (importOriginal) => { const mod = await importOriginal(); const parseHeaderCert = vi.fn( ({ From 804e9d53c948d16a587f4c429b38aee5c1f51960 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Thu, 4 Sep 2025 21:09:35 +0800 Subject: [PATCH 17/17] fix: test --- packages/service/common/mongo/index.ts | 21 +++++++++---------- .../support/permission/evaluation/auth.ts | 2 +- test/mocks/request.ts | 13 +++++++++++- test/setup.ts | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/service/common/mongo/index.ts b/packages/service/common/mongo/index.ts index 6e19f9144eac..5666d6057464 100644 --- a/packages/service/common/mongo/index.ts +++ b/packages/service/common/mongo/index.ts @@ -72,18 +72,17 @@ const addCommonMiddleware = (schema: mongoose.Schema) => { const convertObjectIds = (obj: any) => { if (!obj) return; - // Convert _id and other ObjectId fields in one pass - for (const key in obj) { - const val = obj[key]; - if ( - val && - typeof val === 'object' && - val._bsontype === 'ObjectId' && - typeof val.toString === 'function' - ) { - obj[key] = val.toString(); - } + // Convert _id + if (obj._id && obj._id.toString) { + obj._id = obj._id.toString(); } + + // Convert other ObjectId fields + Object.keys(obj).forEach((key) => { + if (obj[key] && obj[key]._bsontype === 'ObjectId') { + obj[key] = obj[key].toString(); + } + }); }; if (Array.isArray(docs)) { diff --git a/packages/service/support/permission/evaluation/auth.ts b/packages/service/support/permission/evaluation/auth.ts index 1622ef31e7e8..c5d4266a3d33 100644 --- a/packages/service/support/permission/evaluation/auth.ts +++ b/packages/service/support/permission/evaluation/auth.ts @@ -1,4 +1,3 @@ -import { parseHeaderCert } from '../controller'; import { authAppByTmbId } from '../app/auth'; import { ManagePermissionVal, @@ -7,6 +6,7 @@ import { import type { EvaluationSchemaType } from '@fastgpt/global/core/app/evaluation/type'; import type { AuthModeType } from '../type'; import { MongoEvaluation } from '../../../core/app/evaluation/evalSchema'; +import { parseHeaderCert } from '../auth/common'; export const authEval = async ({ evalId, diff --git a/test/mocks/request.ts b/test/mocks/request.ts index a129c9d50ad8..09574c203926 100644 --- a/test/mocks/request.ts +++ b/test/mocks/request.ts @@ -88,9 +88,20 @@ vi.mock(import('@fastgpt/service/support/permission/auth/common'), async (import return Promise.resolve(auth); } ); + + const authCert = async (props: any) => { + const result = await parseHeaderCert(props); + + return { + ...result, + isOwner: true, + canWrite: true + }; + }; return { ...mod, - parseHeaderCert + parseHeaderCert, + authCert }; }); diff --git a/test/setup.ts b/test/setup.ts index d3793afde633..b58b1e90e97a 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -31,7 +31,7 @@ vi.mock(import('@/service/common/system'), async (importOriginal) => { return '0.0.0'; }, readConfigData: async () => { - return readFileSync('@/data/config.json', 'utf-8'); + return readFileSync('projects/app/data/config.json', 'utf-8'); }, initSystemConfig: async () => { // read env from projects/app/.env