V4.12.4 features (#5626)

* fix: push again, user select option button and form input radio content overflow (#5601)

* fix: push again, user select option button and form input radio content overflow

* fix: use useCallback instead of useMemo, fix unnecessary delete

* fix: Move the variable inside the component

* fix: do not pass valueLabel to MySelect

* ui

* del collection api adapt

* refactor: inherit permission (#5529)

* refactor: permission update conflict check function

* refactor(permission): app collaborator update api

* refactor(permission): support app update collaborator

* feat: support fe permission conflict check

* refactor(permission): app permission

* refactor(permission): dataset permission

* refactor(permission): team permission

* chore: fe adjust

* fix: type error

* fix: audit pagiation

* fix: tc

* chore: initv4130

* fix: app/dataset auth logic

* chore: move code

* refactor(permission): remove selfPermission

* fix: mock

* fix: test

* fix: app & dataset auth

* fix: inherit

* test(inheritPermission): test syncChildrenPermission

* prompt editor add list plugin (#5620)

* perf: search result (#5608)

* fix: table size (#5598)

* temp: list value

* backspace

* optimize code

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* fix: fe & member list (#5619)

* chore: initv4130

* fix: MemberItemCard

* fix: MemberItemCard

* chore: fe adjust & init script

* perf: test code

* doc

* fix debug variables (#5617)

* perf: search result (#5608)

* fix: table size (#5598)

* fix debug variables

* fix

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>

* perf: member ui

* fix: inherit bug (#5624)

* refactor(permission): remove getClbsWithInfo, which is useless

* fix: app list privateApp

* fix: get infos

* perf(fe): remove delete icon when it is disable in MemberItemCard

* fix: dataset private dataset

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* perf: auto coupon

* chore: upgrade script & get infos avatar  (#5625)

* fix: get infos

* chore: initv4130

* feat: support WecomRobot publish, and fix AesKey can not save bug (#5526)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* remove test

* rename init shell

* feat: collection page store

* reload sandbox

* pysandbox

* remove log

* chore: remove useless code (#5629)

* chore: remove useless code

* fix: checkConflict

* perf: support hidden type for RoleList

* fix: copy node

* update doc

* fix(permission): some bug (#5632)

* fix: app/dataset list

* fix: inherit bug

* perf: del app;i18n;save chat

* fix: test

* i18n

* fix: sumper overflow return OwnerRoleVal (#5633)

* remove invalid code

* fix: scroll

* fix: objectId

* update next

* update package

* object id

* mock redis

* feat: add redis append to resolve wecom stream response  (#5643)

* feat: resolve conflicts

* fix: add param 'show_publish_wecom'

* feat: abstract out WecomCrypto type

* doc: wecom robot document

* fix: solve instability in AI output

* doc: update some pictures

* feat: remove functions from request.ts to chat.ts and toolCall.ts

* doc: wecom robot doc update

* fix

* delete unused code

* doc: update version and prompt

* feat: remove wecom crypto, delete wecom code in workflow

* feat: delete unused codes

* feat: add redis append method

---------

Co-authored-by: heheer <zhiyu44@qq.com>

* cache per

* fix(test): init team sub when creating mocked user (#5646)

* fix: button is not vertically centered (#5647)

* doc

* fix: gridFs objectId (#5649)

---------

Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: 伍闲犬 <whoeverimf5@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: heheer <zhiyu44@qq.com>
This commit is contained in:
Archer
2025-09-15 20:02:54 +08:00
committed by GitHub
parent c8934e3d22
commit 2ed1545eb5
187 changed files with 3701 additions and 2221 deletions

View File

@@ -1,27 +1,24 @@
import Cookie from 'cookie';
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 { ClientSession, AnyBulkWriteOperation } from '../../common/mongo';
import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { authOpenApiKey } from '../openapi/auth';
import { type FileTokenQuery } from '@fastgpt/global/common/file/type';
import { ManageRoleVal, OwnerRoleVal } from '@fastgpt/global/support/permission/constant';
import { MongoResourcePermission } from './schema';
import { type ClientSession } from 'mongoose';
import type { ResourcePermissionType, ResourceType } 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';
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 { authUserSession } from '../user/session';
import { sumPer } from '@fastgpt/global/support/permission/utils';
import { getCollaboratorId, sumPer } from '@fastgpt/global/support/permission/utils';
import { type SyncChildrenPermissionResourceType } from './inheritPermission';
import { pickCollaboratorIdFields } from './utils';
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';
import { DEFAULT_ORG_AVATAR, DEFAULT_TEAM_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
@@ -31,7 +28,7 @@ import { sumPer } from '@fastgpt/global/support/permission/utils';
* @param resourceId
* @returns PermissionValueType | undefined
*/
export const getResourcePermission = async ({
export const getTmbPermission = async ({
resourceType,
teamId,
tmbId,
@@ -106,17 +103,27 @@ export const getResourcePermission = async ({
return sumPer(...groupPers, ...orgPers);
};
export async function getResourceClbsAndGroups({
resourceId,
/**
* Only get resource's owned clbs, not including parents'.
*/
export async function getResourceOwnedClbs({
resourceType,
teamId,
resourceId,
session
}: {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
teamId: string;
session: ClientSession;
}) {
session?: ClientSession;
} & (
| {
resourceType: 'team';
resourceId?: undefined;
}
| {
resourceType: Omit<PerResourceTypeEnum, 'team'>;
resourceId: ParentIdType;
}
)) {
return MongoResourcePermission.find(
{
resourceId,
@@ -124,282 +131,110 @@ export async function getResourceClbsAndGroups({
teamId
},
undefined,
{ session }
{ ...(session ? { session } : {}) }
).lean();
}
export const getClbsAndGroupsWithInfo = async ({
resourceId,
resourceType,
teamId
export const getClbsInfo = async ({
clbs,
teamId,
ownerTmbId
}: {
clbs: CollaboratorItemType[];
teamId: string;
} & (
| {
resourceId: ParentIdType;
resourceType: Omit<`${PerResourceTypeEnum}`, 'team'>;
}
| {
resourceType: 'team';
resourceId?: undefined;
}
)) =>
Promise.all([
MongoResourcePermission.find({
teamId,
resourceId,
resourceType,
tmbId: {
$exists: true
}
})
.populate<{ tmb: TeamMemberSchema }>({
path: 'tmb',
select: 'name userId avatar'
})
.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()
]);
ownerTmbId?: string;
}): Promise<CollaboratorItemDetailType[]> => {
const tmbIds = [];
const orgIds = [];
const groupIds = [];
export const delResourcePermissionById = (id: string) => {
return MongoResourcePermission.findByIdAndDelete(id);
};
export const delResourcePermission = ({
session,
tmbId,
groupId,
orgId,
...props
}: {
resourceType: PerResourceTypeEnum;
teamId: string;
resourceId: string;
session?: ClientSession;
tmbId?: string;
groupId?: string;
orgId?: string;
}) => {
// either tmbId or groupId or orgId must be provided
if (!tmbId && !groupId && !orgId) {
return Promise.reject(CommonErrEnum.missingParams);
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);
}
return MongoResourcePermission.deleteOne(
{
...(tmbId ? { tmbId } : {}),
...(groupId ? { groupId } : {}),
...(orgId ? { orgId } : {}),
...props
},
{ session }
);
};
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();
/* 下面代码等迁移 */
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 clbs.map((clb) => {
const info = infos.find((info) => info._id === getCollaboratorId(clb));
return {
uid: '',
...clb,
teamId,
tmbId,
apikey,
appId: apiKeyAppId || authorizationAppid,
sourceName
permission: new Permission({
role: clb.permission,
isOwner: Boolean(ownerTmbId && clb.tmbId && ownerTmbId === clb.tmbId)
}),
name: info?.name ?? 'Unknown name',
avatar: info?.avatar || (clb.orgId ? DEFAULT_ORG_AVATAR : DEFAULT_TEAM_AVATAR)
};
}
// 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<FileTokenQuery>((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,
session,
tmbId
}: {
resource: SyncChildrenPermissionResourceType;
resourceType: PerResourceTypeEnum;
// should be provided when inheritPermission is true
session: ClientSession;
tmbId: string;
}) => {
const parentClbs = await getResourceOwnedClbs({
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;
}
return clb;
}),
{
tmbId,
permission: OwnerRoleVal
}
];
const ops: AnyBulkWriteOperation<ResourcePermissionType>[] = [];
for (const clb of collaborators) {
ops.push({
updateOne: {
filter: {
...pickCollaboratorIdFields(clb),
teamId: resource.teamId,
resourceId: resource._id,
resourceType
},
update: {
$set: {
permission: clb.permission
}
},
upsert: true
}
});
}
await MongoResourcePermission.bulkWrite(ops, { session });
};