Permission (#1687)
Some checks failed
Build FastGPT images in Personal warehouse / build-fastgpt-images (push) Has been cancelled

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
This commit is contained in:
Archer 2024-06-04 17:52:00 +08:00 committed by GitHub
parent fcb915c988
commit 19c8a06d51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 2291 additions and 1091 deletions

View File

@ -31,7 +31,6 @@ export type FastGPTFeConfigsType = {
show_openai_account?: boolean;
show_promotion?: boolean;
show_team_chat?: boolean;
hide_app_flow?: boolean;
concatMd?: string;
docUrl?: string;
chatbotUrl?: string;

View File

@ -0,0 +1,12 @@
import { PermissionValueType } from '../../support/permission/type';
export type UpdateAppCollaboratorBody = {
appId: string;
tmbIds: string[];
permission: PermissionValueType;
};
export type AppCollaboratorDeleteParams = {
appId: string;
tmbId: string;
};

View File

@ -7,6 +7,8 @@ import { SelectedDatasetType } from '../workflow/api';
import { DatasetSearchModeEnum } from '../dataset/constants';
import { TeamTagSchema as TeamTagsSchemaType } from '@fastgpt/global/support/user/team/type.d';
import { StoreEdgeItemType } from '../workflow/type/edge';
import { PermissionValueType } from '../../support/permission/type';
import { AppPermission } from '../../support/permission/app/controller';
export type AppSchema = {
_id: string;
@ -27,9 +29,9 @@ export type AppSchema = {
scheduledTriggerConfig?: AppScheduledTriggerConfigType | null;
scheduledTriggerNextTime?: Date;
permission: `${PermissionTypeEnum}`;
inited?: boolean;
teamTags: string[];
defaultPermission: PermissionValueType;
};
export type AppListItemType = {
@ -37,13 +39,12 @@ export type AppListItemType = {
name: string;
avatar: string;
intro: string;
isOwner: boolean;
permission: `${PermissionTypeEnum}`;
defaultPermission: PermissionValueType;
permission: AppPermission;
};
export type AppDetailType = AppSchema & {
isOwner: boolean;
canWrite: boolean;
permission: AppPermission;
};
export type AppSimpleEditFormType = {

View File

@ -0,0 +1,20 @@
import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant';
import { PermissionListType } from '../type';
export enum AppPermissionKeyEnum {}
export const AppPermissionList: PermissionListType = {
[PermissionKeyEnum.read]: {
...PermissionList[PermissionKeyEnum.read],
description: '可使用该应用进行对话'
},
[PermissionKeyEnum.write]: {
...PermissionList[PermissionKeyEnum.write],
description: '可查看和编辑应用'
},
[PermissionKeyEnum.manage]: {
...PermissionList[PermissionKeyEnum.manage],
description: '写权限基础上,可配置发布渠道、查看对话日志、分配该应用权限'
}
};
export const AppDefaultPermission = NullPermission;

View File

@ -0,0 +1,15 @@
import { PerConstructPros, Permission } from '../controller';
import { AppDefaultPermission } from './constant';
export class AppPermission extends Permission {
constructor(props?: PerConstructPros) {
if (!props) {
props = {
per: AppDefaultPermission
};
} else if (!props?.per) {
props.per = AppDefaultPermission;
}
super(props);
}
}

View File

View File

@ -0,0 +1,9 @@
import { PermissionValueType } from './type';
export type CollaboratorItemType = {
teamId: string;
tmbId: string;
permission: PermissionValueType;
name: string;
avatar: string;
};

View File

@ -1,3 +1,6 @@
import { Permission } from './controller';
import { PermissionListType } from './type';
export enum AuthUserTypeEnum {
token = 'token',
root = 'root',
@ -21,8 +24,41 @@ export const PermissionTypeMap = {
}
};
export enum ResourceTypeEnum {
export enum PerResourceTypeEnum {
team = 'team',
app = 'app',
dataset = 'dataset'
}
/* new permission */
export enum PermissionKeyEnum {
read = 'read',
write = 'write',
manage = 'manage'
}
export const PermissionList: PermissionListType = {
[PermissionKeyEnum.read]: {
name: '读权限',
description: '',
value: 0b100,
checkBoxType: 'single'
},
[PermissionKeyEnum.write]: {
name: '写权限',
description: '',
value: 0b110, // 如果某个资源有特殊要求,再重写这个值
checkBoxType: 'single'
},
[PermissionKeyEnum.manage]: {
name: '管理员',
description: '',
value: 0b111,
checkBoxType: 'single'
}
};
export const NullPermission = 0;
export const OwnerPermissionVal = ~0 >>> 0;
export const ReadPermissionVal = PermissionList['read'].value;
export const WritePermissionVal = PermissionList['write'].value;
export const ManagePermissionVal = PermissionList['manage'].value;

View File

@ -0,0 +1,71 @@
import { PermissionValueType } from './type';
import { PermissionList, NullPermission, OwnerPermissionVal } from './constant';
export type PerConstructPros = {
per?: PermissionValueType;
isOwner?: boolean;
};
// the Permission helper class
export class Permission {
value: PermissionValueType;
isOwner: boolean;
hasManagePer: boolean;
hasWritePer: boolean;
hasReadPer: boolean;
constructor(props?: PerConstructPros) {
const { per = NullPermission, isOwner = false } = props || {};
if (isOwner) {
this.value = OwnerPermissionVal;
} else {
this.value = per;
}
this.isOwner = isOwner;
this.hasManagePer = this.checkPer(PermissionList['manage'].value);
this.hasWritePer = this.checkPer(PermissionList['write'].value);
this.hasReadPer = this.checkPer(PermissionList['read'].value);
}
// add permission(s)
// it can be chaining called.
// @example
// const perm = new Permission(permission)
// perm.add(PermissionList['read'])
// perm.add(PermissionList['read'], PermissionList['write'])
// perm.add(PermissionList['read']).add(PermissionList['write'])
addPer(...perList: PermissionValueType[]) {
for (let oer of perList) {
this.value = this.value | oer;
}
this.updatePermissions();
return this.value;
}
removePer(...perList: PermissionValueType[]) {
for (let per of perList) {
this.value = this.value & ~per;
}
this.updatePermissions();
return this.value;
}
checkPer(perm: PermissionValueType): boolean {
// if the permission is owner permission, only owner has this permission.
if (perm === OwnerPermissionVal) {
return this.value === OwnerPermissionVal;
} else if (this.hasManagePer) {
// The manager has all permissions except the owner permission
return true;
}
return (this.value & perm) === perm;
}
private updatePermissions() {
this.isOwner = this.value === OwnerPermissionVal;
this.hasManagePer = this.checkPer(PermissionList['manage'].value);
this.hasWritePer = this.checkPer(PermissionList['write'].value);
this.hasReadPer = this.checkPer(PermissionList['read'].value);
}
}

View File

@ -1,6 +1,20 @@
import { AuthUserTypeEnum } from './constant';
import { TeamMemberWithUserSchema } from '../user/team/type';
import { AuthUserTypeEnum, PermissionKeyEnum } 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.
// The lowest 3 bits present the permission of reading, writing and managing.
// The higher bits are advanced permissions or extended permissions, which could be customized.
export type PermissionValueType = number;
export type PermissionListType<T = {}> = Record<
T | PermissionKeyEnum,
{
name: string;
description: string;
value: PermissionValueType;
checkBoxType: 'single' | 'multiple';
}
>;
export type AuthResponseType = {
teamId: string;
@ -17,4 +31,9 @@ export type ResourcePermissionType = {
tmbId: string;
resourceType: ResourceType;
permission: PermissionValueType;
resourceId: string;
};
export type ResourcePerWithTmbWithUser = Omit<ResourcePermissionType, 'tmbId'> & {
tmbId: TeamMemberWithUserSchema;
};

View File

@ -0,0 +1,16 @@
import { PermissionKeyEnum, PermissionList, ReadPermissionVal } from '../constant';
export const TeamPermissionList = {
[PermissionKeyEnum.read]: {
...PermissionList[PermissionKeyEnum.read]
},
[PermissionKeyEnum.write]: {
...PermissionList[PermissionKeyEnum.write]
},
[PermissionKeyEnum.manage]: {
...PermissionList[PermissionKeyEnum.manage],
description: '可邀请, 删除成员'
}
};
export const TeamDefaultPermissionVal = ReadPermissionVal;

View File

@ -0,0 +1,15 @@
import { PerConstructPros, Permission } from '../controller';
import { TeamDefaultPermissionVal } from './constant';
export class TeamPermission extends Permission {
constructor(props?: PerConstructPros) {
if (!props) {
props = {
per: TeamDefaultPermissionVal
};
} else if (!props?.per) {
props.per = TeamDefaultPermissionVal;
}
super(props);
}
}

View File

@ -1,22 +1,25 @@
import { TeamMemberRoleEnum } from '../user/team/constant';
import { PermissionTypeEnum } from './constant';
import { Permission } from './controller';
/* team public source, or owner source in team */
export function mongoRPermission({
teamId,
tmbId,
role
permission
}: {
teamId: string;
tmbId: string;
role: `${TeamMemberRoleEnum}`;
permission: Permission;
}) {
if (permission.isOwner) {
return {
teamId
};
}
return {
teamId,
...(role === TeamMemberRoleEnum.visitor && { permission: PermissionTypeEnum.public }),
...(role === TeamMemberRoleEnum.admin && {
$or: [{ permission: PermissionTypeEnum.public }, { tmbId }]
})
$or: [{ permission: PermissionTypeEnum.public }, { tmbId }]
};
}
export function mongoOwnerPermission({ teamId, tmbId }: { teamId: string; tmbId: string }) {

View File

@ -1,4 +1,4 @@
import { PermissionValueType } from 'support/permission/type';
import { PermissionValueType } from '../../permission/type';
import { TeamMemberRoleEnum } from './constant';
import { LafAccountType, TeamMemberSchema } from './type';
@ -22,7 +22,6 @@ export type UpdateTeamProps = {
/* ------------- member ----------- */
export type DelMemberProps = {
teamId: string;
memberId: string;
};
export type UpdateTeamMemberProps = {
@ -46,7 +45,6 @@ export type InviteMemberResponse = Record<
>;
export type UpdateTeamMemberPermissionProps = {
teamId: string;
memberIds: string[];
permission: PermissionValueType;
};

View File

@ -2,6 +2,7 @@ import type { UserModelSchema } from '../type';
import type { TeamMemberRoleEnum, TeamMemberStatusEnum } from './constant';
import { LafAccountType } from './type';
import { PermissionValueType, ResourcePermissionType } from '../../permission/type';
import { TeamPermission } from '../../permission/user/controller';
export type TeamSchema = {
_id: string;
@ -49,7 +50,7 @@ export type TeamMemberWithTeamAndUserSchema = Omit<TeamMemberWithTeamSchema, 'us
userId: UserModelSchema;
};
export type TeamItemType = {
export type TeamTmbItemType = {
userId: string;
teamId: string;
teamName: string;
@ -61,9 +62,8 @@ export type TeamItemType = {
defaultTeam: boolean;
role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`;
canWrite: boolean;
lafAccount?: LafAccountType;
defaultPermission: PermissionValueType;
permission: TeamPermission;
};
export type TeamMemberItemType = {
@ -75,7 +75,7 @@ export type TeamMemberItemType = {
// TODO: this should be deprecated.
role: `${TeamMemberRoleEnum}`;
status: `${TeamMemberStatusEnum}`;
permission: PermissionValueType;
permission: TeamPermission;
};
export type TeamTagItemType = {

View File

@ -1,5 +1,5 @@
import { UserStatusEnum } from './constant';
import { TeamItemType } from './team/type';
import { TeamTmbItemType } from './team/type';
export type UserModelSchema = {
_id: string;
@ -29,6 +29,6 @@ export type UserType = {
timezone: string;
promotionRate: UserModelSchema['promotionRate'];
openaiAccount: UserModelSchema['openaiAccount'];
team: TeamItemType;
team: TeamTmbItemType;
standardInfo?: standardInfoType;
};

View File

@ -7,6 +7,7 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { AppDefaultPermission } from '@fastgpt/global/support/permission/app/constant';
export const AppCollectionName = 'apps';
@ -98,6 +99,12 @@ const AppSchema = new Schema({
inited: {
type: Boolean
},
// the default permission of a app
defaultPermission: {
type: Number,
default: AppDefaultPermission
}
});

View File

@ -0,0 +1,85 @@
/* Auth app permission */
import { MongoApp } from '../../../core/app/schema';
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import { AuthPropsType } from '../type/auth.d';
import { parseHeaderCert } from '../controller';
import { PerResourceTypeEnum } 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 { AppPermission } from '@fastgpt/global/support/permission/app/controller';
import { AuthResponseType } from '../type/auth.d';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
export const authAppByTmbId = async ({
teamId,
tmbId,
appId,
per
}: {
teamId: string;
tmbId: string;
appId: string;
per: PermissionValueType;
}) => {
const { permission: tmbPer } = await getTmbInfoByTmbId({ tmbId });
const app = await (async () => {
// get app and per
const [app, rp] = await Promise.all([
MongoApp.findOne({ _id: appId, teamId }).lean(),
getResourcePermission({
teamId,
tmbId,
resourceId: appId,
resourceType: PerResourceTypeEnum.app
}) // this could be null
]);
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
const isOwner = tmbPer.isOwner || String(app.tmbId) === tmbId;
const Per = new AppPermission({ per: rp?.permission ?? app.defaultPermission, isOwner });
if (!Per.checkPer(per)) {
return Promise.reject(AppErrEnum.unAuthApp);
}
return {
...app,
permission: Per
};
})();
return { app };
};
export const authApp = async ({
appId,
per,
...props
}: AuthPropsType & {
appId: string;
}): Promise<
AuthResponseType & {
app: AppDetailType;
}
> => {
const result = await parseHeaderCert(props);
const { teamId, tmbId } = result;
const { app } = await authAppByTmbId({
teamId,
tmbId,
appId,
per
});
return {
...result,
permission: app.permission,
app
};
};

View File

@ -1,72 +0,0 @@
import { MongoApp } from '../../../core/app/schema';
import { AppDetailType } from '@fastgpt/global/core/app/type.d';
import { AuthModeType } from '../type';
import { AuthResponseType } from '@fastgpt/global/support/permission/type';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { parseHeaderCert } from '../controller';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { getTmbInfoByTmbId } from '../../user/team/controller';
// 模型使用权校验
export async function authApp({
appId,
per = 'owner',
...props
}: AuthModeType & {
appId: string;
}): Promise<
AuthResponseType & {
teamOwner: boolean;
app: AppDetailType;
role: `${TeamMemberRoleEnum}`;
}
> {
const result = await parseHeaderCert(props);
const { teamId, tmbId } = result;
const { role } = await getTmbInfoByTmbId({ tmbId });
const { app, isOwner, canWrite } = await (async () => {
// get app
const app = await MongoApp.findOne({ _id: appId, teamId }).lean();
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
const isOwner = String(app.tmbId) === tmbId || role === TeamMemberRoleEnum.owner;
const canWrite =
isOwner ||
(app.permission === PermissionTypeEnum.public && role !== TeamMemberRoleEnum.visitor);
if (per === 'r') {
if (!isOwner && app.permission !== PermissionTypeEnum.public) {
return Promise.reject(AppErrEnum.unAuthApp);
}
}
if (per === 'w' && !canWrite) {
return Promise.reject(AppErrEnum.unAuthApp);
}
if (per === 'owner' && !isOwner) {
return Promise.reject(AppErrEnum.unAuthApp);
}
return {
app: {
...app,
isOwner,
canWrite
},
isOwner,
canWrite
};
})();
return {
...result,
app,
role,
isOwner,
canWrite,
teamOwner: role === TeamMemberRoleEnum.owner
};
}

View File

@ -1,96 +0,0 @@
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { AuthModeType } from '../type';
import { AuthResponseType } from '@fastgpt/global/support/permission/type';
import { AppDetailType } from '@fastgpt/global/core/app/type';
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
import { parseHeaderCert } from '../controller';
import { MongoOutLink } from '../../outLink/schema';
import { MongoApp } from '../../../core/app/schema';
import { OutLinkErrEnum } from '@fastgpt/global/common/error/code/outLink';
import { PermissionTypeEnum } from '@fastgpt/global/support/permission/constant';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { getTmbInfoByTmbId } from '../../user/team/controller';
/* crud outlink permission */
export async function authOutLinkCrud({
outLinkId,
per = 'owner',
...props
}: AuthModeType & {
outLinkId: string;
}): Promise<
AuthResponseType & {
app: AppDetailType;
outLink: OutLinkSchema;
}
> {
const result = await parseHeaderCert(props);
const { tmbId, teamId } = result;
const { role } = await getTmbInfoByTmbId({ tmbId });
const { app, outLink, isOwner, canWrite } = await (async () => {
const outLink = await MongoOutLink.findOne({ _id: outLinkId, teamId });
if (!outLink) {
throw new Error(OutLinkErrEnum.unExist);
}
const app = await MongoApp.findById(outLink.appId);
if (!app) {
return Promise.reject(AppErrEnum.unExist);
}
const isOwner = String(outLink.tmbId) === tmbId || role === TeamMemberRoleEnum.owner;
const canWrite =
isOwner ||
(app.permission === PermissionTypeEnum.public && role !== TeamMemberRoleEnum.visitor);
if (per === 'r' && !isOwner && app.permission !== PermissionTypeEnum.public) {
return Promise.reject(OutLinkErrEnum.unAuthLink);
}
if (per === 'w' && !canWrite) {
return Promise.reject(OutLinkErrEnum.unAuthLink);
}
if (per === 'owner' && !isOwner) {
return Promise.reject(OutLinkErrEnum.unAuthLink);
}
return {
app: {
...app,
isOwner: String(app.tmbId) === tmbId,
canWrite
},
outLink,
isOwner,
canWrite
};
})();
return {
...result,
app,
outLink,
isOwner,
canWrite
};
}
/* outLink exist and it app exist */
export async function authOutLinkValid({ shareId }: { shareId?: string }) {
if (!shareId) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const shareChat = await MongoOutLink.findOne({ shareId });
if (!shareChat) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
return {
appId: shareChat.appId,
shareChat
};
}

View File

@ -1,66 +0,0 @@
import { AuthResponseType } from '@fastgpt/global/support/permission/type';
import { AuthModeType } from '../type';
import { TeamItemType } from '@fastgpt/global/support/user/team/type';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { parseHeaderCert } from '../controller';
import { getTmbInfoByTmbId } from '../../user/team/controller';
import { UserErrEnum } from '../../../../global/common/error/code/user';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
export async function authUserNotVisitor(props: AuthModeType): Promise<
AuthResponseType & {
team: TeamItemType;
role: `${TeamMemberRoleEnum}`;
}
> {
const { teamId, tmbId } = await parseHeaderCert(props);
const team = await getTmbInfoByTmbId({ tmbId });
if (team.role === TeamMemberRoleEnum.visitor) {
return Promise.reject(UserErrEnum.binVisitor);
}
return {
teamId,
tmbId,
team,
role: team.role,
isOwner: team.role === TeamMemberRoleEnum.owner, // teamOwner
canWrite: true
};
}
/* auth user role */
export async function authUserRole(props: AuthModeType): Promise<
AuthResponseType & {
role: `${TeamMemberRoleEnum}`;
teamOwner: boolean;
}
> {
const result = await parseHeaderCert(props);
const { role: userRole, canWrite } = await getTmbInfoByTmbId({ tmbId: result.tmbId });
return {
...result,
isOwner: true,
role: userRole,
teamOwner: userRole === TeamMemberRoleEnum.owner,
canWrite
};
}
/* auth teamMember in team role */
export async function authTeamOwner(props: AuthModeType): Promise<
AuthResponseType & {
role: `${TeamMemberRoleEnum}`;
teamOwner: boolean;
}
> {
const authRes = await authUserRole(props);
if (authRes.role !== TeamMemberRoleEnum.owner) {
return Promise.reject(TeamErrEnum.unAuthTeam);
}
return authRes;
}

View File

@ -3,10 +3,76 @@ import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import jwt from 'jsonwebtoken';
import { NextApiResponse } from 'next';
import type { AuthModeType, ReqHeaderAuthType } from './type.d';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { AuthUserTypeEnum, PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { authOpenApiKey } from '../openapi/auth';
import { FileTokenQuery } from '@fastgpt/global/common/file/type';
import { MongoResourcePermission } from './schema';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
export const getResourcePermission = async ({
resourceType,
teamId,
tmbId,
resourceId
}: {
resourceType: PerResourceTypeEnum;
teamId: string;
tmbId: string;
resourceId?: string;
}) => {
const per = await MongoResourcePermission.findOne({
tmbId,
teamId,
resourceType,
resourceId
});
if (!per) {
return null;
}
return per;
};
export const delResourcePermissionById = (id: string) => {
return MongoResourcePermission.findByIdAndRemove(id);
};
export const updateResourcePermission = async ({
resourceId,
resourceType,
teamId,
tmbIdList,
permission
}: {
resourceId?: string;
resourceType: PerResourceTypeEnum;
teamId: string;
tmbIdList: string[];
permission: PermissionValueType;
}) => {
await mongoSessionRun((session) => {
return Promise.all(
tmbIdList.map((tmbId) =>
MongoResourcePermission.findOneAndUpdate(
{
resourceType,
teamId,
tmbId,
resourceId
},
{
permission
},
{
session,
upsert: true
}
)
)
);
});
};
/* 下面代码等迁移 */
/* create token */
export function createJWT(user: { _id?: string; team?: { teamId?: string; tmbId: string } }) {
const key = process.env.TOKEN_KEY as string;

View File

@ -0,0 +1,69 @@
import { AppDetailType } from '@fastgpt/global/core/app/type';
import { 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 { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { AuthPropsType } from '../type/auth';
import { AuthResponseType } from '../type/auth';
import { authAppByTmbId } from '../app/auth';
/* crud outlink permission */
export async function authOutLinkCrud({
outLinkId,
per,
...props
}: AuthPropsType & {
outLinkId: string;
}): Promise<
AuthResponseType & {
app: AppDetailType;
outLink: OutLinkSchema;
}
> {
const result = await parseHeaderCert(props);
const { tmbId, teamId } = result;
const { app, outLink } = await (async () => {
const outLink = await MongoOutLink.findOne({ _id: outLinkId, teamId });
if (!outLink) {
throw new Error(OutLinkErrEnum.unExist);
}
const { app } = await authAppByTmbId({
teamId,
tmbId,
appId: outLink.appId,
per: ManagePermissionVal
});
return {
outLink,
app
};
})();
return {
...result,
permission: app.permission,
app,
outLink
};
}
/* outLink exist and it app exist */
export async function authOutLinkValid({ shareId }: { shareId?: string }) {
if (!shareId) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
const shareChat = await MongoOutLink.findOne({ shareId });
if (!shareChat) {
return Promise.reject(OutLinkErrEnum.linkUnInvalid);
}
return {
appId: shareChat.appId,
shareChat
};
}

View File

@ -1,16 +0,0 @@
import { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
import { MongoResourcePermission } from './schema';
import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
export async function getResourcePermission({
tmbId,
resourceType
}: {
tmbId: string;
resourceType: ResourceTypeEnum;
}) {
return (await MongoResourcePermission.findOne({
tmbId,
resourceType
})) as ResourcePermissionType;
}

View File

@ -1,127 +0,0 @@
// 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.
// The lowest 3 bits present the permission of reading, writing and managing.
// The higher bits are advanced permissions or extended permissions, which could be customized.
export type PermissionValueType = number;
export type PermissionListType = { [key: string]: PermissionValueType };
export const NullPermission: PermissionValueType = 0;
// the Permission helper class
export class Permission {
value: PermissionValueType;
constructor(value: PermissionValueType) {
this.value = value;
}
// add permission(s)
// it can be chaining called.
// @example
// const perm = new Permission(permission)
// perm.add(PermissionList['read'])
// perm.add(PermissionList['read'], PermissionList['write'])
// perm.add(PermissionList['read']).add(PermissionList['write'])
add(...perm: PermissionValueType[]): Permission {
for (let p of perm) {
this.value = addPermission(this.value, p);
}
return this;
}
remove(...perm: PermissionValueType[]): Permission {
for (let p of perm) {
this.value = removePermission(this.value, p);
}
return this;
}
check(perm: PermissionValueType): Permission | boolean {
if (checkPermission(this.value, perm)) {
return this;
} else {
return false;
}
}
}
export function constructPermission(permList: PermissionValueType[]) {
return new Permission(NullPermission).add(...permList);
}
// The base Permissions List
// It can be extended, for example:
// export const UserPermissionList: PermissionListType = {
// ...PermissionList,
// 'Invite': 0b1000
// }
export const PermissionList: PermissionListType = {
Read: 0b100,
Write: 0b010,
Manage: 0b001
};
// list of permissions. could be customized.
// ! removal of the basic permissions is not recommended.
// const PermList: Array<PermissionType> = [ReadPerm, WritePerm, ManagePerm];
// return the list of permissions
// @param Perm(optional): the list of permissions to be added
// export function getPermList(Perm?: PermissionType[]): Array<PermissionType> {
// if (Perm === undefined) {
// return PermList;
// } else {
// return PermList.concat(Perm);
// }
// }
// check the permission
// @param [val]: The permission value to be checked
// @parma [perm]: Which Permission value will be checked
// @returns [booean]: if the [val] has the [perm]
// example:
// const perm = user.permission // get this permisiion from db or somewhere else
// const ok = checkPermission(perm, PermissionList['Read'])
export function checkPermission(val: PermissionValueType, perm: PermissionValueType): boolean {
return (val & perm) === perm;
}
// add the permission
// it can be chaining called.
// return the new permission value based on [val] added with [perm]
// @param val: PermissionValueType
// @param perm: PermissionValueType
// example:
// const basePerm = 0b001; // Manage only
export function addPermission(
val: PermissionValueType,
perm: PermissionValueType
): PermissionValueType {
return val | perm;
}
// remove the permission
export function removePermission(
val: PermissionValueType,
perm: PermissionValueType
): PermissionValueType {
return val & ~perm;
}
// export function parsePermission(val: PermissionValueType, list: PermissionValueType[]) {
// const result: [[string, boolean]] = [] as any;
// list.forEach((perm) => {
// result.push([perm[0], checkPermission(val, perm)]);
// });
// return result;
// }
export function hasManage(val: PermissionValueType) {
return checkPermission(val, PermissionList['Manage']);
}
export function hasWrite(val: PermissionValueType) {
return checkPermission(val, PermissionList['Write']);
}
export function hasRead(val: PermissionValueType) {
return checkPermission(val, PermissionList['Read']);
}

View File

@ -2,11 +2,13 @@ import {
TeamCollectionName,
TeamMemberCollectionName
} from '@fastgpt/global/support/user/team/constant';
import { Model, connectionMongo } from '../../../common/mongo';
import { Model, connectionMongo } from '../../common/mongo';
import type { ResourcePermissionType } from '@fastgpt/global/support/permission/type';
import { ResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
const { Schema, model, models } = connectionMongo;
export const ResourcePermissionCollectionName = 'resource_permission';
export const ResourcePermissionSchema = new Schema({
teamId: {
type: Schema.Types.ObjectId,
@ -17,30 +19,36 @@ export const ResourcePermissionSchema = new Schema({
ref: TeamMemberCollectionName
},
resourceType: {
type: Object.values(ResourceTypeEnum),
type: Object.values(PerResourceTypeEnum),
required: true
},
permission: {
type: Number,
required: true
},
// Resrouce ID: App or DataSet or any other resource type.
// It is null if the resourceType is team.
resourceId: {
type: Schema.Types.ObjectId
}
});
try {
ResourcePermissionSchema.index({
teamId: 1,
resourceType: 1
});
ResourcePermissionSchema.index({
tmbId: 1,
resourceType: 1
});
ResourcePermissionSchema.index(
{
resourceType: 1,
teamId: 1,
tmbId: 1,
resourceId: 1
},
{
unique: true
}
);
} catch (error) {
console.log(error);
}
export const ResourcePermissionCollectionName = 'resource_permission';
export const MongoResourcePermission: Model<ResourcePermissionType> =
models[ResourcePermissionCollectionName] ||
model(ResourcePermissionCollectionName, ResourcePermissionSchema);

View File

@ -1,4 +1,3 @@
import { getVectorCountByTeamId } from '../../common/vectorStore/controller';
import { getTeamPlanStatus, getTeamStandPlan } from '../../support/wallet/sub/utils';
import { MongoApp } from '../../core/app/schema';
import { MongoPlugin } from '../../core/plugin/schema';

View File

@ -1,4 +1,5 @@
import { ApiRequestProps } from '../../type/next';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
export type ReqHeaderAuthType = {
cookie?: string;
@ -8,10 +9,11 @@ export type ReqHeaderAuthType = {
userid?: string;
authorization?: string;
};
export type AuthModeType = {
req: ApiRequestProps;
authToken?: boolean;
authRoot?: boolean;
authApiKey?: boolean;
per?: 'r' | 'w' | 'owner';
per?: PermissionValueType | 'r' | 'w' | 'owner'; // this is for compatibility
};

View File

@ -0,0 +1,21 @@
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { ApiRequestProps } from '../../../type/next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
export type AuthPropsType = {
req: ApiRequestProps;
authToken?: boolean;
authRoot?: boolean;
authApiKey?: boolean;
per: PermissionValueType;
};
export type AuthResponseType = {
teamId: string;
tmbId: string;
authType?: `${AuthUserTypeEnum}`;
appId?: string;
apikey?: string;
permission: Permission;
};

View File

@ -0,0 +1,26 @@
import { AuthResponseType } from '../type/auth.d';
import { AuthPropsType } from '../type/auth.d';
import { 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';
/* auth user role */
export async function authUserPer(props: AuthPropsType): Promise<
AuthResponseType & {
tmb: TeamTmbItemType;
}
> {
const result = await parseHeaderCert(props);
const tmb = await getTmbInfoByTmbId({ tmbId: result.tmbId });
if (!tmb.permission.checkPer(props.per)) {
return Promise.reject(TeamErrEnum.unAuthTeam);
}
return {
...result,
permission: tmb.permission,
tmb
};
}

View File

@ -1,4 +1,4 @@
import { TeamItemType, TeamMemberWithTeamSchema } from '@fastgpt/global/support/user/team/type';
import { TeamTmbItemType, TeamMemberWithTeamSchema } from '@fastgpt/global/support/user/team/type';
import { ClientSession, Types } from '../../../common/mongo';
import {
TeamMemberRoleEnum,
@ -8,13 +8,25 @@ import {
import { MongoTeamMember } from './teamMemberSchema';
import { MongoTeam } from './teamSchema';
import { UpdateTeamProps } from '@fastgpt/global/support/user/team/controller';
import { getResourcePermission } from '../../permission/controller';
import {
PerResourceTypeEnum,
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { TeamPermission } from '@fastgpt/global/support/permission/user/controller';
async function getTeamMember(match: Record<string, any>): Promise<TeamItemType> {
async function getTeamMember(match: Record<string, any>): Promise<TeamTmbItemType> {
const tmb = (await MongoTeamMember.findOne(match).populate('teamId')) as TeamMemberWithTeamSchema;
if (!tmb) {
return Promise.reject('member not exist');
}
const tmbPer = await getResourcePermission({
resourceType: PerResourceTypeEnum.team,
teamId: tmb.teamId._id,
tmbId: tmb._id
});
return {
userId: String(tmb.userId),
teamId: String(tmb.teamId._id),
@ -27,9 +39,11 @@ async function getTeamMember(match: Record<string, any>): Promise<TeamItemType>
role: tmb.role,
status: tmb.status,
defaultTeam: tmb.defaultTeam,
canWrite: tmb.role !== TeamMemberRoleEnum.visitor,
lafAccount: tmb.teamId.lafAccount,
defaultPermission: tmb.teamId.defaultPermission
permission: new TeamPermission({
per: tmbPer?.permission ?? tmb.teamId.defaultPermission,
isOwner: tmb.role === TeamMemberRoleEnum.owner
})
};
}

View File

@ -3,7 +3,7 @@ const { Schema, model, models } = connectionMongo;
import { TeamSchema as TeamType } from '@fastgpt/global/support/user/team/type.d';
import { userCollectionName } from '../../user/schema';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { NullPermission } from '../../permission/resourcePermission/permisson';
import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/user/constant';
const TeamSchema = new Schema({
name: {
@ -16,7 +16,7 @@ const TeamSchema = new Schema({
},
defaultPermission: {
type: Number,
default: NullPermission
default: TeamDefaultPermissionVal
},
avatar: {
type: String,

View File

@ -180,6 +180,7 @@ export const iconPaths = {
kbTest: () => import('./icons/kbTest.svg'),
menu: () => import('./icons/menu.svg'),
minus: () => import('./icons/minus.svg'),
'modal/AddClb': () => import('./icons/modal/AddClb.svg'),
'modal/concat': () => import('./icons/modal/concat.svg'),
'modal/confirmPay': () => import('./icons/modal/confirmPay.svg'),
'modal/edit': () => import('./icons/modal/edit.svg'),

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8.00303 3.64414C6.69861 3.64414 5.64117 4.70158 5.64117 6.006C5.64117 7.31042 6.69861 8.36786 8.00303 8.36786C9.30744 8.36786 10.3649 7.31042 10.3649 6.006C10.3649 4.70158 9.30744 3.64414 8.00303 3.64414ZM3.9745 6.006C3.9745 3.78111 5.77813 1.97748 8.00303 1.97748C10.2279 1.97748 12.0315 3.78111 12.0315 6.006C12.0315 8.23089 10.2279 10.0345 8.00303 10.0345C5.77813 10.0345 3.9745 8.23089 3.9745 6.006ZM12.0234 2.73039C12.1961 2.30378 12.6819 2.09793 13.1085 2.27062C14.5833 2.86762 15.6261 4.31403 15.6261 6.006C15.6261 7.69797 14.5833 9.14439 13.1085 9.74138C12.6819 9.91407 12.1961 9.70823 12.0234 9.28161C11.8507 8.855 12.0565 8.36917 12.4831 8.19649C13.3502 7.84549 13.9595 6.9959 13.9595 6.006C13.9595 5.01611 13.3502 4.16651 12.4831 3.81552C12.0565 3.64283 11.8507 3.157 12.0234 2.73039ZM6.77537 11.563H10C10.4603 11.563 10.8334 11.9361 10.8334 12.3964C10.8334 12.8566 10.4603 13.2297 10 13.2297H6.80483C6.04904 13.2297 5.52394 13.2302 5.11329 13.2582C4.71013 13.2857 4.47852 13.337 4.30339 13.4095C3.72467 13.6492 3.26488 14.109 3.02516 14.6877C2.95262 14.8629 2.90135 15.0945 2.87385 15.4976C2.84583 15.9083 2.84538 16.4334 2.84538 17.1892C2.84538 17.6494 2.47228 18.0225 2.01204 18.0225C1.55181 18.0225 1.17871 17.6494 1.17871 17.1892L1.17871 17.1597C1.17871 16.4403 1.1787 15.8583 1.21105 15.3842C1.24434 14.8962 1.31469 14.462 1.48536 14.0499C1.89424 13.0628 2.67848 12.2786 3.66559 11.8697C4.07764 11.699 4.51182 11.6287 4.99984 11.5954C5.47392 11.563 6.05597 11.563 6.77537 11.563ZM15.5916 11.563C16.0518 11.563 16.4249 11.9361 16.4249 12.3964V13.9594H17.988C18.4482 13.9594 18.8213 14.3325 18.8213 14.7928C18.8213 15.253 18.4482 15.6261 17.988 15.6261H16.4249V17.1892C16.4249 17.6494 16.0518 18.0225 15.5916 18.0225C15.1314 18.0225 14.7583 17.6494 14.7583 17.1892V15.6261H13.1952C12.735 15.6261 12.3619 15.253 12.3619 14.7928C12.3619 14.3325 12.735 13.9594 13.1952 13.9594H14.7583V12.3964C14.7583 11.9361 15.1314 11.563 15.5916 11.563Z"
fill="#3370FF" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,5 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.00303 2.64414C5.69861 2.64414 4.64117 3.70158 4.64117 5.006C4.64117 6.31042 5.69861 7.36786 7.00303 7.36786C8.30744 7.36786 9.36488 6.31042 9.36488 5.006C9.36488 3.70158 8.30744 2.64414 7.00303 2.64414ZM2.9745 5.006C2.9745 2.78111 4.77813 0.977478 7.00303 0.977478C9.22792 0.977478 11.0315 2.78111 11.0315 5.006C11.0315 7.23089 9.22792 9.03453 7.00303 9.03453C4.77813 9.03453 2.9745 7.23089 2.9745 5.006ZM11.0234 1.73039C11.1961 1.30378 11.6819 1.09793 12.1085 1.27062C13.5833 1.86762 14.6261 3.31403 14.6261 5.006C14.6261 6.69797 13.5833 8.14439 12.1085 8.74138C11.6819 8.91407 11.1961 8.70823 11.0234 8.28161C10.8507 7.855 11.0565 7.36917 11.4831 7.19649C12.3502 6.84549 12.9595 5.9959 12.9595 5.006C12.9595 4.01611 12.3502 3.16651 11.4831 2.81552C11.0565 2.64283 10.8507 2.157 11.0234 1.73039ZM5.77537 10.563H9.00002C9.46026 10.563 9.83335 10.9361 9.83335 11.3964C9.83335 11.8566 9.46026 12.2297 9.00002 12.2297H5.80483C5.04904 12.2297 4.52394 12.2302 4.11329 12.2582C3.71013 12.2857 3.47852 12.337 3.30339 12.4095C2.72467 12.6492 2.26488 13.109 2.02516 13.6877C1.95262 13.8629 1.90135 14.0945 1.87385 14.4976C1.84583 14.9083 1.84538 15.4334 1.84538 16.1892C1.84538 16.6494 1.47228 17.0225 1.01204 17.0225C0.551807 17.0225 0.178711 16.6494 0.178711 16.1892L0.178711 16.1597C0.178705 15.4403 0.1787 14.8583 0.211047 14.3842C0.244344 13.8962 0.314685 13.462 0.485364 13.0499C0.894235 12.0628 1.67848 11.2786 2.66559 10.8697C3.07764 10.699 3.51182 10.6287 3.99984 10.5954C4.47392 10.563 5.05597 10.563 5.77537 10.563ZM14.5916 10.563C15.0518 10.563 15.4249 10.9361 15.4249 11.3964V12.9594H16.988C17.4482 12.9594 17.8213 13.3325 17.8213 13.7928C17.8213 14.253 17.4482 14.6261 16.988 14.6261H15.4249V16.1892C15.4249 16.6494 15.0518 17.0225 14.5916 17.0225C14.1314 17.0225 13.7583 16.6494 13.7583 16.1892V14.6261H12.1952C11.735 14.6261 11.3619 14.253 11.3619 13.7928C11.3619 13.3325 11.735 12.9594 12.1952 12.9594H13.7583V11.3964C13.7583 10.9361 14.1314 10.563 14.5916 10.563Z"
fill="#3370FF" />
</svg>
d="M7.00303 2.64414C5.69861 2.64414 4.64117 3.70158 4.64117 5.006C4.64117 6.31042 5.69861 7.36786 7.00303 7.36786C8.30744 7.36786 9.36488 6.31042 9.36488 5.006C9.36488 3.70158 8.30744 2.64414 7.00303 2.64414ZM2.9745 5.006C2.9745 2.78111 4.77813 0.977478 7.00303 0.977478C9.22792 0.977478 11.0315 2.78111 11.0315 5.006C11.0315 7.23089 9.22792 9.03453 7.00303 9.03453C4.77813 9.03453 2.9745 7.23089 2.9745 5.006ZM11.0234 1.73039C11.1961 1.30378 11.6819 1.09793 12.1085 1.27062C13.5833 1.86762 14.6261 3.31403 14.6261 5.006C14.6261 6.69797 13.5833 8.14439 12.1085 8.74138C11.6819 8.91407 11.1961 8.70823 11.0234 8.28161C10.8507 7.855 11.0565 7.36917 11.4831 7.19649C12.3502 6.84549 12.9595 5.9959 12.9595 5.006C12.9595 4.01611 12.3502 3.16651 11.4831 2.81552C11.0565 2.64283 10.8507 2.157 11.0234 1.73039ZM5.77537 10.563H9.00002C9.46026 10.563 9.83335 10.9361 9.83335 11.3964C9.83335 11.8566 9.46026 12.2297 9.00002 12.2297H5.80483C5.04904 12.2297 4.52394 12.2302 4.11329 12.2582C3.71013 12.2857 3.47852 12.337 3.30339 12.4095C2.72467 12.6492 2.26488 13.109 2.02516 13.6877C1.95262 13.8629 1.90135 14.0945 1.87385 14.4976C1.84583 14.9083 1.84538 15.4334 1.84538 16.1892C1.84538 16.6494 1.47228 17.0225 1.01204 17.0225C0.551807 17.0225 0.178711 16.6494 0.178711 16.1892L0.178711 16.1597C0.178705 15.4403 0.1787 14.8583 0.211047 14.3842C0.244344 13.8962 0.314685 13.462 0.485364 13.0499C0.894235 12.0628 1.67848 11.2786 2.66559 10.8697C3.07764 10.699 3.51182 10.6287 3.99984 10.5954C4.47392 10.563 5.05597 10.563 5.77537 10.563ZM14.5916 10.563C15.0518 10.563 15.4249 10.9361 15.4249 11.3964V12.9594H16.988C17.4482 12.9594 17.8213 13.3325 17.8213 13.7928C17.8213 14.253 17.4482 14.6261 16.988 14.6261H15.4249V16.1892C15.4249 16.6494 15.0518 17.0225 14.5916 17.0225C14.1314 17.0225 13.7583 16.6494 13.7583 16.1892V14.6261H12.1952C11.735 14.6261 11.3619 14.253 11.3619 13.7928C11.3619 13.3325 11.735 12.9594 12.1952 12.9594H13.7583V11.3964C13.7583 10.9361 14.1314 10.563 14.5916 10.563Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,16 +1,17 @@
import React, { forwardRef } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Box, BoxProps, SpinnerProps } from '@chakra-ui/react';
import Loading from '../MyLoading';
type Props = BoxProps & {
isLoading?: boolean;
text?: string;
size?: SpinnerProps['size'];
};
const MyBox = ({ text, isLoading, children, ...props }: Props, ref: any) => {
const MyBox = ({ text, isLoading, children, size, ...props }: Props, ref: any) => {
return (
<Box ref={ref} position={isLoading ? 'relative' : 'unset'} {...props}>
{isLoading && <Loading fixed={false} text={text} />}
{isLoading && <Loading fixed={false} text={text} size={size} />}
{children}
</Box>
);

View File

@ -1,16 +1,18 @@
import React from 'react';
import { Spinner, Flex, Box } from '@chakra-ui/react';
import { Spinner, Flex, Box, SpinnerProps } from '@chakra-ui/react';
const Loading = ({
fixed = true,
text = '',
bg = 'rgba(255,255,255,0.5)',
zIndex = 1000
zIndex = 1000,
size = 'xl'
}: {
fixed?: boolean;
text?: string;
bg?: string;
zIndex?: number;
size?: SpinnerProps['size'];
}) => {
return (
<Flex
@ -31,7 +33,7 @@ const Loading = ({
speed="0.65s"
emptyColor="myGray.100"
color="primary.500"
size="xl"
size={size}
/>
{text && (
<Box mt={2} color="primary.600" fontWeight={'bold'}>

View File

@ -16,12 +16,12 @@ import { useLoading } from '../../../hooks/useLoading';
import MyIcon from '../Icon';
export type SelectProps = ButtonProps & {
value?: string;
value?: string | number;
placeholder?: string;
list: {
alias?: string;
label: string | React.ReactNode;
value: string;
value: string | number;
}[];
isLoading?: boolean;
onchange?: (val: any) => void;

View File

@ -1212,6 +1212,8 @@
"Tools": "Tools"
},
"permission": {
"Default permission": "Default permission",
"Manage": "Manage",
"Private": "Private",
"Private Tip": "Only available to oneself",
"Public": "Team",

View File

@ -1 +1,5 @@
{}
{
"team": {
"Add manager": "Add manager"
}
}

View File

@ -585,7 +585,8 @@
"success": "开始同步"
}
},
"training": {}
"training": {
}
},
"data": {
"Auxiliary Data": "辅助数据",
@ -1218,6 +1219,8 @@
"Tools": "工具"
},
"permission": {
"Default permission": "默认权限",
"Manage": "管理",
"Private": "私有",
"Private Tip": "仅自己可用",
"Public": "团队",
@ -1574,7 +1577,7 @@
"Processing invitations": "处理邀请",
"Processing invitations Tips": "你有{{amount}}个需要处理的团队邀请",
"Reinvite": "重新邀请",
"Remove Member Confirm Tip": "确认将 {{username}} 移出团队?其所有资源将转让到团队创建者的账户内。",
"Remove Member Confirm Tip": "确认将 {{username}} 移出团队?",
"Remove Member Failed": "移除团队成员异常",
"Remove Member Success": "移除团队成员成功",
"Remove Member Tip": "移出团队",

View File

@ -1 +1,5 @@
{}
{
"team": {
"Add manager": "添加管理员"
}
}

View File

@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1700634007483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19559" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M65.361 856H967v160H65.361zM65.531 805.062l2.295-188.972L676.402 7.515 863.078 194.19 254.503 802.766 65.53 805.062z m50.726-169.52l-1.46 120.254 120.254-1.46L116.257 635.54z m507.147-507.147L742.198 247.19l52.163-52.163L675.567 76.232l-52.163 52.163z" fill="#1AA5FF" p-id="19560"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1700634007483" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19559" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M65.361 856H967v160H65.361zM65.531 805.062l2.295-188.972L676.402 7.515 863.078 194.19 254.503 802.766 65.53 805.062z m50.726-169.52l-1.46 120.254 120.254-1.46L116.257 635.54z m507.147-507.147L742.198 247.19l52.163-52.163L675.567 76.232l-52.163 52.163z" p-id="19560"></path></svg>

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 614 B

View File

@ -0,0 +1,49 @@
import { Box, BoxProps } from '@chakra-ui/react';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useTranslation } from 'next-i18next';
import React from 'react';
import type { PermissionValueType } from '@fastgpt/global/support/permission/type';
export enum defaultPermissionEnum {
private = 'private',
read = 'read',
edit = 'edit'
}
type Props = Omit<BoxProps, 'onChange'> & {
per: PermissionValueType;
defaultPer: PermissionValueType;
readPer: PermissionValueType;
writePer: PermissionValueType;
onChange: (v: PermissionValueType) => void;
};
const DefaultPermissionList = ({
per,
defaultPer,
readPer,
writePer,
onChange,
...styles
}: Props) => {
const { t } = useTranslation();
const defaultPermissionSelectList = [
{ label: '仅协作者访问', value: defaultPer },
{ label: '团队可访问', value: readPer },
{ label: '团队可编辑', value: writePer }
];
return (
<Box {...styles}>
<MySelect
list={defaultPermissionSelectList}
value={per}
onchange={(v) => {
onChange(v);
}}
/>
</Box>
);
};
export default DefaultPermissionList;

View File

@ -1,19 +1,34 @@
import React from 'react';
import React, { useMemo } from 'react';
import { PermissionTypeEnum, PermissionTypeMap } from '@fastgpt/global/support/permission/constant';
import { Box, Flex, FlexProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { Permission } from '@fastgpt/global/support/permission/controller';
const PermissionIconText = ({
permission,
defaultPermission,
...props
}: { permission: `${PermissionTypeEnum}` } & FlexProps) => {
}: {
permission?: `${PermissionTypeEnum}`;
defaultPermission?: PermissionValueType;
} & FlexProps) => {
const { t } = useTranslation();
return PermissionTypeMap[permission] ? (
const per = useMemo(() => {
if (permission) return permission;
if (defaultPermission) {
return new Permission({ per: defaultPermission }).hasReadPer ? 'public' : 'private';
}
return 'private';
}, [defaultPermission, permission]);
return PermissionTypeMap[per] ? (
<Flex alignItems={'center'} {...props}>
<MyIcon name={PermissionTypeMap[permission]?.iconLight as any} w={'14px'} />
<MyIcon name={PermissionTypeMap[per]?.iconLight as any} w={'14px'} />
<Box ml={'2px'} lineHeight={1}>
{t(PermissionTypeMap[permission]?.label)}
{t(PermissionTypeMap[per]?.label)}
</Box>
</Flex>
) : null;

View File

@ -0,0 +1,216 @@
import {
Flex,
Box,
Grid,
ModalBody,
InputGroup,
InputLeftElement,
Input,
Checkbox,
ModalFooter,
Button,
useToast
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import MyAvatar from '@/components/Avatar';
import { useMemo, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getTeamMembers } from '@/web/support/user/team/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { ChevronDownIcon } from '@chakra-ui/icons';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
export type AddModalPropsType = {
onClose: () => void;
};
export function AddMemberModal({ onClose }: AddModalPropsType) {
const toast = useToast();
const { userInfo } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPreLabelList } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const {
data: members = [],
refetch: refetchMembers,
isLoading: loadingMembers
} = useQuery(['getMembers', userInfo?.team?.teamId], async () => {
if (!userInfo?.team?.teamId) return [];
const members = await getTeamMembers();
return members;
});
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.permission.isOwner) return false;
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
});
}, [members, searchText, userInfo?.team?.tmbId]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPreLabelList(selectedPermission).join('、');
}, [getPreLabelList, selectedPermission]);
const { mutate: onConfirm, isLoading: isUpdating } = useRequest({
mutationFn: () => {
return onUpdateCollaborators(selectedMemberIdList, selectedPermission);
},
successToast: '添加成功',
errorToast: 'Error',
onSuccess() {
onClose();
}
});
return (
<MyModal isOpen onClose={onClose} iconSrc="modal/AddClb" title="添加协作者" minW="800px">
<ModalBody>
<MyBox
isLoading={loadingMembers}
display={'grid'}
minH="400px"
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="55% 45%"
>
<Flex
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
minH="200px"
>
<InputGroup alignItems="center" h="32px" my="2" py="1">
<InputLeftElement>
<MyIcon name="common/searchLight" w="16px" color={'myGray.500'} />
</InputLeftElement>
<Input
placeholder="搜索用户名"
fontSize="lg"
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
</InputGroup>
<Flex flexDirection="column" mt="2">
{filterMembers.map((member) => {
const onChange = () => {
if (selectedMemberIdList.includes(member.tmbId)) {
setSelectedMembers(selectedMemberIdList.filter((v) => v !== member.tmbId));
} else {
setSelectedMembers([...selectedMemberIdList, member.tmbId]);
}
};
const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId);
return (
<Flex
key={member.tmbId}
mt="1"
py="1"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer'
}}
>
<Checkbox
size="lg"
mr="3"
isChecked={selectedMemberIdList.includes(member.tmbId)}
onChange={onChange}
/>
<Flex
flexDirection="row"
onClick={onChange}
w="full"
justifyContent="space-between"
>
<Flex flexDirection="row" alignItems="center">
<MyAvatar src={member.avatar} w="32px" />
<Box ml="2">{member.memberName}</Box>
</Flex>
{!!collaborator && <PermissionTags permission={collaborator.permission} />}
</Flex>
</Flex>
);
})}
</Flex>
</Flex>
<Flex p="4" flexDirection="column">
<Box>: {selectedMemberIdList.length}</Box>
<Flex flexDirection="column" mt="2">
{selectedMemberIdList.map((tmbId) => {
const member = filterMembers.find((v) => v.tmbId === tmbId);
return member ? (
<Flex
key={tmbId}
alignItems="center"
justifyContent="space-between"
py="2"
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.50' }}
_notLast={{ mb: 2 }}
>
<Avatar src={member.avatar} w="24px" />
<Box w="full" fontSize="lg">
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
/>
</Flex>
) : null;
})}
</Flex>
</Flex>
</MyBox>
</ModalBody>
<ModalFooter>
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{perLabel}
<ChevronDownIcon fontSize={'lg'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
</Button>
</ModalFooter>
</MyModal>
);
}

View File

@ -0,0 +1,119 @@
import {
ModalBody,
Table,
TableContainer,
Tbody,
Th,
Thead,
Tr,
Td,
Box,
Flex
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import Avatar from '@/components/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
export type ManageModalProps = {
onClose: () => void;
};
function ManageModal({ onClose }: ManageModalProps) {
const { userInfo } = useUserStore();
const { collaboratorList, onUpdateCollaborators, onDelOneCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
const { mutate: onDelete, isLoading: isDeleting } = useRequest({
mutationFn: (tmbId: string) => onDelOneCollaborator(tmbId)
});
const { mutate: onUpdate, isLoading: isUpdating } = useRequest({
mutationFn: ({ tmbId, per }: { tmbId: string; per: PermissionValueType }) => {
return onUpdateCollaborators([tmbId], per);
},
successToast: '更新成功',
errorToast: 'Error'
});
const loading = isDeleting || isUpdating;
return (
<MyModal
isLoading={loading}
isOpen
onClose={onClose}
minW="600px"
title="管理协作者"
iconSrc="common/settingLight"
>
<ModalBody>
<TableContainer borderRadius="md" minH="400px">
<Table>
<Thead bg="myGray.100">
<Tr>
<Th border="none"></Th>
<Th border="none"></Th>
<Th border="none"></Th>
</Tr>
</Thead>
<Tbody>
{collaboratorList?.map((item) => {
return (
<Tr
key={item.tmbId}
_hover={{
bg: 'myGray.50'
}}
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
{item.name}
</Flex>
</Td>
<Td border="none">
<PermissionTags permission={item.permission} />
</Td>
<Td border="none">
{item.tmbId !== userInfo?.team?.tmbId && (
<PermissionSelect
Button={
<MyIcon name={'edit'} w={'16px'} _hover={{ color: 'primary.600' }} />
}
value={item.permission}
onChange={(per) => {
onUpdate({
tmbId: item.tmbId,
per
});
}}
onDelete={() => {
onDelete(item.tmbId);
}}
/>
)}
</Td>
</Tr>
);
})}
</Tbody>
</Table>
{collaboratorList?.length === 0 && <EmptyTip text={'暂无协作者'} />}
</TableContainer>
</ModalBody>
</MyModal>
);
}
export default ManageModal;

View File

@ -0,0 +1,243 @@
import {
ButtonProps,
Flex,
Menu,
MenuButton,
MenuList,
Box,
Radio,
useOutsideClick
} from '@chakra-ui/react';
import React, { useMemo, useRef, useState } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useContextSelector } from 'use-context-selector';
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';
export type PermissionSelectProps = {
value?: PermissionValueType;
onChange: (value: PermissionValueType) => void;
trigger?: 'hover' | 'click';
offset?: [number, number];
Button: React.ReactNode;
onDelete?: () => void;
} & Omit<ButtonProps, 'onChange' | 'value'>;
const MenuStyle = {
py: 2,
px: 3,
_hover: {
bg: 'myGray.50'
},
borderRadius: 'md',
cursor: 'pointer',
fontSize: 'sm'
};
function PermissionSelect({
value,
onChange,
trigger = 'click',
offset = [0, 5],
Button,
width = 'auto',
onDelete,
...props
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLDivElement>(null);
const closeTimer = useRef<any>();
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
const list = Object.entries(permissionList).map(([key, value]) => {
return {
name: value.name,
value: value.value,
description: value.description,
checkBoxType: value.checkBoxType
};
});
return {
singleCheckBoxList: list.filter((item) => item.checkBoxType === 'single'),
multipleCheckBoxList: list.filter((item) => item.checkBoxType === 'multiple')
};
}, [permissionList]);
const selectedSingleValue = useMemo(() => {
const per = new Permission({ per: value });
if (per.hasManagePer) return permissionList['manage'].value;
if (per.hasWritePer) return permissionList['write'].value;
return permissionList['read'].value;
}, [permissionList, value]);
const selectedMultipleValues = useMemo(() => {
const per = new Permission({ per: value });
return permissionSelectList.multipleCheckBoxList
.filter((item) => {
return per.checkPer(item.value);
})
.map((item) => item.value);
}, [permissionSelectList.multipleCheckBoxList, value]);
useOutsideClick({
ref: ref,
handler: () => {
setIsOpen(false);
}
});
return (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
ref={ref}
onMouseEnter={() => {
if (trigger === 'hover') {
setIsOpen(true);
}
clearTimeout(closeTimer.current);
}}
onMouseLeave={() => {
if (trigger === 'hover') {
closeTimer.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}
}}
>
<Box
position={'relative'}
onClickCapture={() => {
if (trigger === 'click') {
setIsOpen(!isOpen);
}
}}
>
<MenuButton
w={'100%'}
h={'100%'}
position={'absolute'}
top={0}
right={0}
bottom={0}
left={0}
/>
<Box position={'relative'} cursor={'pointer'} userSelect={'none'}>
{Button}
</Box>
</Box>
<MenuList
minW={isOpen ? `${width}px !important` : 0}
p="3"
border={'1px solid #fff'}
boxShadow={
'0px 2px 4px rgba(161, 167, 179, 0.25), 0px 0px 1px rgba(121, 141, 159, 0.25);'
}
zIndex={99}
overflowY={'auto'}
>
{/* The list of single select permissions */}
{permissionSelectList.singleCheckBoxList.map((item) => {
const change = () => {
const per = new Permission({ per: value });
per.removePer(selectedSingleValue);
per.addPer(item.value);
onChange(per.value);
setIsOpen(false);
};
return (
<Flex
key={item.value}
{...(selectedSingleValue === item.value
? {
color: 'primary.600'
}
: {})}
{...MenuStyle}
onClick={change}
maxW={['70vw', '300px']}
>
<Radio size="lg" isChecked={selectedSingleValue === item.value} />
<Box ml={4}>
<Box>{item.name}</Box>
<Box color={'myGray.500'}>{item.description}</Box>
</Box>
</Flex>
);
})}
{/* <MyDivider my={3} />
{multipleValues.length > 0 && <Box m="4"></Box>} */}
{/* The list of multiple select permissions */}
{/* {list
.filter((item) => item.type === 'multiple')
.map((item) => {
const change = () => {
if (checkPermission(valueState, item.value)) {
setValueState(new Permission(valueState).remove(item.value).value);
} else {
setValueState(new Permission(valueState).add(item.value).value);
}
};
return (
<Flex
key={item.value}
{...(checkPermission(valueState, item.value)
? {
color: 'primary.500',
bg: 'myWhite.300'
}
: {})}
whiteSpace="pre-wrap"
flexDirection="row"
justifyContent="start"
p="2"
_hover={{
bg: 'myGray.50'
}}
>
<Checkbox
size="lg"
isChecked={checkPermission(valueState, item.value)}
onChange={change}
/>
<Flex px="4" flexDirection="column" onClick={change}>
<Box fontWeight="500">{item.name}</Box>
<Box fontWeight="400">{item.description}</Box>
</Flex>
</Flex>
);
})}*/}
{onDelete && (
<>
<MyDivider my={2} h={'2px'} borderColor={'myGray.200'} />
<Flex
{...MenuStyle}
onClick={() => {
onDelete();
setIsOpen(false);
}}
>
<MyIcon name="delete" w="20px" color="red.600" />
<Box color="red.600">{t('common.Delete')}</Box>
</Flex>
</>
)}
</MenuList>
</Box>
</Menu>
);
}
export default React.memo(PermissionSelect);

View File

@ -0,0 +1,36 @@
import { Flex } from '@chakra-ui/react';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import Tag from '@fastgpt/web/components/common/Tag';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
export type PermissionTagsProp = {
permission: PermissionValueType;
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPreLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const perTagList = getPreLabelList(permission);
return (
<Flex gap="2" alignItems="center">
{perTagList.map((item) => (
<Tag
mixBlendMode={'multiply'}
key={item}
colorSchema="blue"
border="none"
py={2}
px={3}
fontSize={'xs'}
>
{item}
</Tag>
))}
</Flex>
);
}
export default PermissionTags;

View File

@ -0,0 +1,107 @@
import { CollaboratorItemType } from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { useQuery } from '@tanstack/react-query';
import { ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
export type MemberManagerInputPropsType = {
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
onUpdateCollaborators: (tmbIds: string[], permission: PermissionValueType) => any;
onDelOneCollaborator: (tmbId: string) => any;
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
collaboratorList: CollaboratorItemType[];
refetchCollaboratorList: () => void;
isFetchingCollaborator: boolean;
getPreLabelList: (per: PermissionValueType) => string[];
};
type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
throw new Error('Function not implemented.');
},
getPreLabelList: function (): string[] {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false
});
export const CollaboratorContextProvider = ({
onGetCollaboratorList,
permissionList,
onUpdateCollaborators,
onDelOneCollaborator,
children
}: MemberManagerInputPropsType & {
children: ReactNode;
}) => {
const {
data: collaboratorList = [],
refetch: refetchCollaboratorList,
isLoading: isFetchingCollaborator
} = useQuery(['collaboratorList'], onGetCollaboratorList);
const onUpdateCollaboratorsThen = async (tmbIds: string[], permission: PermissionValueType) => {
await onUpdateCollaborators(tmbIds, permission);
refetchCollaboratorList();
};
const onDelOneCollaboratorThem = async (tmbId: string) => {
await onDelOneCollaborator(tmbId);
refetchCollaboratorList();
};
const getPreLabelList = useCallback(
(per: PermissionValueType) => {
const Per = new Permission({ per });
const labels: string[] = [];
if (Per.hasManagePer) {
labels.push(permissionList['manage'].name);
} else if (Per.hasWritePer) {
labels.push(permissionList['write'].name);
} else {
labels.push(permissionList['read'].name);
}
Object.values(permissionList).forEach((item) => {
if (item.checkBoxType === 'multiple') {
if (Per.checkPer(item.value)) {
labels.push(item.name);
}
}
});
return labels;
},
[permissionList]
);
const contextValue = {
onGetCollaboratorList,
collaboratorList,
refetchCollaboratorList,
isFetchingCollaborator,
permissionList,
onUpdateCollaborators: onUpdateCollaboratorsThen,
onDelOneCollaborator: onDelOneCollaboratorThem,
getPreLabelList
};
return (
<CollaboratorContext.Provider value={contextValue}>{children}</CollaboratorContext.Provider>
);
};

View File

@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Flex, Box, Button, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { AddMemberModal } from './AddMemberModal';
import { useContextSelector } from 'use-context-selector';
import ManageModal from './ManageModal';
import {
CollaboratorContext,
CollaboratorContextProvider,
MemberManagerInputPropsType
} from './context';
import { useTranslation } from 'next-i18next';
import MyBox from '@fastgpt/web/components/common/MyBox';
function MemberManger() {
const { t } = useTranslation();
const {
isOpen: isOpenAddMember,
onOpen: onOpenAddMember,
onClose: onCloseAddMember
} = useDisclosure();
const {
isOpen: isOpenManageModal,
onOpen: onOpenManageModal,
onClose: onCloseManageModal
} = useDisclosure();
const { collaboratorList, isFetchingCollaborator } = useContextSelector(
CollaboratorContext,
(v) => v
);
return (
<>
<Flex alignItems="center" flexDirection="row" justifyContent="space-between" w="full">
<Box></Box>
<Flex flexDirection="row" gap="2">
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="common/settingLight" />}
onClick={onOpenManageModal}
>
{t('permission.Manage')}
</Button>
<Button
size="sm"
variant="whitePrimary"
leftIcon={<MyIcon w="4" name="support/permission/collaborator" />}
onClick={onOpenAddMember}
>
{t('common.Add')}
</Button>
</Flex>
</Flex>
{/* member list */}
<MyBox
isLoading={isFetchingCollaborator}
mt={2}
bg="myGray.100"
borderRadius="md"
size={'md'}
>
{collaboratorList?.length === 0 ? (
<Box p={3} color="myGray.600" fontSize={'xs'} textAlign={'center'}>
</Box>
) : (
<Flex gap="2" p={1.5}>
{collaboratorList?.map((member) => {
return (
<Tag px="4" py="1.5" bgColor="white" key={member.tmbId} width="fit-content">
<Flex alignItems="center">
<Avatar src={member.avatar} w="24px" />
<TagLabel mx="2">{member.name}</TagLabel>
</Flex>
</Tag>
);
})}
</Flex>
)}
</MyBox>
{isOpenAddMember && <AddMemberModal onClose={onCloseAddMember} />}
{isOpenManageModal && <ManageModal onClose={onCloseManageModal} />}
</>
);
}
function Render(props: MemberManagerInputPropsType) {
return (
<CollaboratorContextProvider {...props}>
<MemberManger />
</CollaboratorContextProvider>
);
}
export default React.memo(Render);

View File

@ -1,103 +0,0 @@
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
function MemberTable() {
const members = useContextSelector(TeamContext, (v) => v.members);
const openRemoveMember = useContextSelector(TeamContext, (v) => v.openRemoveMember);
const onRemoveMember = useContextSelector(TeamContext, (v) => v.onRemoveMember);
const { userInfo } = useUserStore();
const { t } = useTranslation();
return (
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th>{t('common.Action')}</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{hasManage(
members.find((item) => item.tmbId === userInfo?.team.tmbId)?.permission!
) &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<MyMenu
width={20}
trigger="hover"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
);
}
export default MemberTable;

View File

@ -1,72 +1,94 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import { Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamContext } from '.';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import RowTabs from '../../../../common/Rowtabs';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { DragHandleIcon } from '@chakra-ui/icons';
import MemberTable from './MemberTable';
import PermissionManage from './PermissionManage';
import MemberTable from './components/MemberTable';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { hasManage } from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useI18n } from '@/web/context/I18n';
type TabListType = Pick<React.ComponentProps<typeof RowTabs>, 'list'>['list'];
import { TeamModalContext } from './context';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api';
import dynamic from 'next/dynamic';
enum TabListEnum {
member = 'member',
permission = 'permission'
}
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
const InviteModal = dynamic(() => import('./components/InviteModal'));
const PermissionManage = dynamic(() => import('./components/PermissionManage/index'));
function TeamCard() {
const { toast } = useToast();
const { t } = useTranslation();
const { userT } = useI18n();
const members = useContextSelector(TeamContext, (v) => v.members);
const onOpenInvite = useContextSelector(TeamContext, (v) => v.onOpenInvite);
const onOpenTeamTagsAsync = useContextSelector(TeamContext, (v) => v.onOpenTeamTagsAsync);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const onLeaveTeam = useContextSelector(TeamContext, (v) => v.onLeaveTeam);
const { myTeams, refetchTeams, members, refetchMembers, setEditTeamData, onSwitchTeam } =
useContextSelector(TeamModalContext, (v) => v);
const { userInfo, teamPlanStatus } = useUserStore();
const { feConfigs } = useSystemStore();
const { ConfirmModal: ConfirmLeaveTeamModal, openConfirm: openLeaveConfirm } = useConfirm({
content: t('user.team.member.Confirm Leave')
});
const { feConfigs } = useSystemStore();
const Tablist: TabListType = [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
if (!teamId) return;
const defaultTeam = myTeams.find((item) => item.defaultTeam) || myTeams[0];
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
];
onSuccess() {
refetchTeams();
},
errorToast: t('user.team.Leave Team Failed')
});
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const Tablist = useMemo(
() => [
{
icon: 'support/team/memberLight',
label: (
<Flex alignItems={'center'}>
<Box ml={1}>{t('user.team.Member')}</Box>
<Box ml={2} bg={'myGray.100'} borderRadius={'20px'} px={3} fontSize={'xs'}>
{members.length}
</Box>
</Flex>
),
value: TabListEnum.member
},
{
icon: 'support/team/key',
label: t('common.Role'),
value: TabListEnum.permission
}
],
[members.length, t]
);
const [tab, setTab] = useState(Tablist[0].value);
const [tab, setTab] = useState<string>(Tablist[0].value);
return (
<Flex
flexDirection={'column'}
flex={'1'}
h={['auto', '100%']}
bg={'white'}
minH={['50vh', 'auto']}
h={'100%'}
borderRadius={['8px 8px 0 0', '8px 0 0 8px']}
>
<Flex
@ -107,41 +129,38 @@ function TeamCard() {
list={Tablist}
value={tab}
onChange={(v) => {
setTab(v as string);
setTab(v as TabListEnum);
}}
></RowTabs>
{/* ctrl buttons */}
<Flex alignItems={'center'}>
{hasManage(
members.find((item) => item.tmbId.toString() === userInfo?.team.tmbId.toString())
?.permission!
) &&
tab === 'member' && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo?.team.role === TeamMemberRoleEnum.owner && feConfigs?.show_team_chat && (
{tab === TabListEnum.member && userInfo?.team.permission.hasManagePer && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name="common/inviteLight" w={'14px'} color={'primary.500'} />}
onClick={() => {
if (
teamPlanStatus?.standardConstants?.maxTeamMember &&
teamPlanStatus.standardConstants.maxTeamMember <= members.length
) {
toast({
status: 'warning',
title: t('user.team.Over Max Member Tip', {
max: teamPlanStatus.standardConstants.maxTeamMember
})
});
} else {
onOpenInvite();
}
}}
>
{t('user.team.Invite Member')}
</Button>
)}
{userInfo?.team.permission.hasManagePer && feConfigs?.show_team_chat && (
<Button
variant={'whitePrimary'}
size="sm"
@ -155,13 +174,14 @@ function TeamCard() {
{t('user.team.Team Tags Async')}
</Button>
)}
{userInfo?.team.role !== TeamMemberRoleEnum.owner && (
{!userInfo?.team.permission.isOwner && (
<Button
variant={'whitePrimary'}
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'support/account/loginoutLight'} w={'14px'} />}
isLoading={isLoadingLeaveTeam}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
}}
@ -173,8 +193,18 @@ function TeamCard() {
</Flex>
<Box mt={3} flex={'1 0 0'} overflow={'auto'}>
{tab === 'member' ? <MemberTable /> : <PermissionManage />}
{tab === TabListEnum.member && <MemberTable />}
{tab === TabListEnum.permission && <PermissionManage />}
</Box>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmLeaveTeamModal />
</Flex>
);

View File

@ -3,20 +3,15 @@ import Avatar from '@/components/Avatar';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import EditModal, { defaultForm } from './EditModal';
import { defaultForm } from './components/EditInfoModal';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import { TeamModalContext } from './context';
function TeamList() {
const { t } = useTranslation();
const { userInfo, initUserInfo } = useUserStore();
const editTeamData = useContextSelector(TeamContext, (v) => v.editTeamData);
const setEditTeamData = useContextSelector(TeamContext, (v) => v.setEditTeamData);
const myTeams = useContextSelector(TeamContext, (v) => v.myTeams);
const refetchTeam = useContextSelector(TeamContext, (v) => v.refetchTeam);
const onSwitchTeam = useContextSelector(TeamContext, (v) => v.onSwitchTeam);
// get the list of teams
const { userInfo } = useUserStore();
const { myTeams, onSwitchTeam, setEditTeamData } = useContextSelector(TeamModalContext, (v) => v);
return (
<Flex
@ -100,16 +95,6 @@ function TeamList() {
</Flex>
))}
</Box>
{!!editTeamData && (
<EditModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeam();
initUserInfo();
}}
/>
)}
</Flex>
);
}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
@ -14,7 +14,7 @@ import { postCreateTeam, putUpdateTeam } from '@/web/support/user/team/api';
import { CreateTeamProps } from '@fastgpt/global/support/user/team/controller.d';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
export type FormDataType = CreateTeamProps & {
export type EditTeamFormDataType = CreateTeamProps & {
id?: string;
};
@ -28,17 +28,17 @@ function EditModal({
onClose,
onSuccess
}: {
defaultData?: FormDataType;
defaultData?: EditTeamFormDataType;
onClose: () => void;
onSuccess: () => void;
}) {
const { t } = useTranslation();
const [refresh, setRefresh] = useState(false);
const { toast } = useToast();
const { register, setValue, getValues, handleSubmit } = useForm<CreateTeamProps>({
const { register, setValue, handleSubmit, watch } = useForm<CreateTeamProps>({
defaultValues: defaultData
});
const avatar = watch('avatar');
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png,.svg',
@ -57,7 +57,6 @@ function EditModal({
maxH: 300
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.Select File Failed')),
@ -80,7 +79,7 @@ function EditModal({
errorToast: t('common.Create Failed')
});
const { mutate: onclickUpdate, isLoading: updating } = useRequest({
mutationFn: async (data: FormDataType) => {
mutationFn: async (data: EditTeamFormDataType) => {
if (!data.id) return Promise.resolve('');
return putUpdateTeam({
name: data.name,
@ -110,7 +109,7 @@ function EditModal({
<MyTooltip label={t('common.Set Avatar')}>
<Avatar
flexShrink={0}
src={getValues('avatar')}
src={avatar}
w={['28px', '32px']}
h={['28px', '32px']}
cursor={'pointer'}

View File

@ -0,0 +1,116 @@
import Avatar from '@/components/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, MenuButton, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import {
TeamMemberRoleEnum,
TeamMemberRoleMap,
TeamMemberStatusMap
} from '@fastgpt/global/support/user/team/constant';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useUserStore } from '@/web/support/user/useUserStore';
import { TeamModalContext } from '../context';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { delRemoveMember } from '@/web/support/user/team/api';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import MyBox from '@fastgpt/web/components/common/MyBox';
function MemberTable() {
const { userInfo } = useUserStore();
const { t } = useTranslation();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm({});
const { mutate: onRemoveMember, isLoading: isRemovingMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
refetchMembers();
},
successToast: t('user.team.Remove Member Success'),
errorToast: t('user.team.Remove Member Failed')
});
return (
<MyBox isLoading={isRemovingMember}>
<TableContainer overflow={'unset'}>
<Table overflow={'unset'}>
<Thead bg={'myWhite.400'}>
<Tr>
<Th>{t('common.Username')}</Th>
<Th>{t('user.team.Role')}</Th>
<Th>{t('common.Status')}</Th>
<Th>{t('common.Action')}</Th>
</Tr>
</Thead>
<Tbody>
{members.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td display={'flex'} alignItems={'center'}>
<Avatar src={item.avatar} w={['18px', '22px']} />
<Box flex={'1 0 0'} w={0} ml={1} className={'textEllipsis'}>
{item.memberName}
</Box>
</Td>
<Td>{t(TeamMemberRoleMap[item.role]?.label || '')}</Td>
<Td color={TeamMemberStatusMap[item.status].color}>
{t(TeamMemberStatusMap[item.status]?.label || '')}
</Td>
<Td>
{userInfo?.team.permission.hasManagePer &&
item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<MyMenu
width={20}
trigger="hover"
Button={
<MenuButton
_hover={{
bg: 'myWhite.600'
}}
borderRadius={'md'}
px={2}
py={1}
lineHeight={1}
>
<MyIcon
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'primary.500' }}
/>
</MenuButton>
}
menuList={[
{
label: t('user.team.Remove Member Tip'),
onClick: () =>
openRemoveMember(
() =>
onRemoveMember({
teamId: item.teamId,
memberId: item.tmbId
}),
undefined,
t('user.team.Remove Member Confirm Tip', {
username: item.memberName
})
)()
}
]}
/>
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
<ConfirmRemoveMemberModal />
</TableContainer>
</MyBox>
);
}
export default MemberTable;

View File

@ -14,43 +14,30 @@ import {
} from '@chakra-ui/react';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '.';
import {
hasManage,
constructPermission,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import { TeamModalContext } from '../../context';
import { useI18n } from '@/web/context/I18n';
function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
const { t } = useTranslation();
const { userT } = useI18n();
const { userInfo } = useUserStore();
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const members = useContextSelector(TeamContext, (v) =>
v.members.filter((member) => {
return member.tmbId != userInfo!.team.tmbId && !hasManage(member.permission);
})
);
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const [selected, setSelected] = useState<typeof members>([]);
const [search, setSearch] = useState<string>('');
const [searched, setSearched] = useState<typeof members>(members);
const [searchKey, setSearchKey] = useState('');
const { mutate: submit, isLoading } = useRequest({
mutationFn: async () => {
console.log(selected);
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([
PermissionList['Read'],
PermissionList['Write'],
PermissionList['Manage']
]).value,
permission: ManagePermissionVal,
memberIds: selected.map((item) => {
return item.tmbId;
})
@ -63,18 +50,23 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
successToast: '成功',
errorToast: '失败'
});
const filterMembers = useMemo(() => {
return members.filter((member) => {
if (member.permission.isOwner) return false;
if (!searchKey) return true;
return !!member.memberName.includes(searchKey);
});
}, [members, searchKey]);
return (
<MyModal
isOpen
iconSrc={'support/permission/collaborator'}
iconSrc={'modal/AddClb'}
maxW={['90vw']}
minW={['900px']}
overflow={'unset'}
title={
<Box>
<Box></Box>
</Box>
}
title={userT('team.Add manager')}
>
<ModalCloseButton onClick={onClose} />
<ModalBody py={6} px={10}>
@ -95,15 +87,12 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
fontSize="lg"
bg={'myGray.50'}
onChange={(e) => {
setSearch(e.target.value);
setSearched(
members.filter((member) => member.memberName.includes(e.target.value))
);
setSearchKey(e.target.value);
}}
/>
</InputGroup>
<Flex flexDirection="column" mt={3}>
{searched.map((member) => {
{filterMembers.map((member) => {
return (
<Flex
py="2"
@ -164,7 +153,7 @@ function AddManagerModal({ onClose, onSuccess }: { onClose: () => void; onSucces
</ModalBody>
<ModalFooter alignItems="flex-end">
<Button h={'30px'} isLoading={isLoading} onClick={submit}>
{t('common.Confirm')}
</Button>
</ModalFooter>
</MyModal>

View File

@ -1,26 +1,24 @@
import React from 'react';
import { Box, Button, Flex, Tag, TagLabel, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { TeamContext } from '.';
import { useContextSelector } from 'use-context-selector';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import AddManagerModal from './AddManager';
import { updateMemberPermission } from '@/web/support/user/team/api';
import { delMemberPermission } from '@/web/support/user/team/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import {
constructPermission,
hasManage,
PermissionList
} from '@fastgpt/service/support/permission/resourcePermission/permisson';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import { TeamModalContext } from '../../context';
import { TeamPermissionList } from '@fastgpt/global/support/permission/user/constant';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
const AddManagerModal = dynamic(() => import('./AddManager'));
function PermissionManage() {
const { t } = useTranslation();
const members = useContextSelector(TeamContext, (v) => v.members);
const refetchMembers = useContextSelector(TeamContext, (v) => v.refetchMembers);
const { userInfo } = useUserStore();
const { members, refetchMembers } = useContextSelector(TeamModalContext, (v) => v);
const {
isOpen: isOpenAddManager,
@ -28,30 +26,19 @@ function PermissionManage() {
onClose: onCloseAddManager
} = useDisclosure();
const { mutate: removeManager } = useRequest({
const { mutate: removeManager, isLoading: isRemovingManager } = useRequest({
mutationFn: async (memberId: string) => {
return updateMemberPermission({
teamId: userInfo!.team.teamId,
permission: constructPermission([PermissionList['Read'], PermissionList['Write']]).value,
memberIds: [memberId]
});
return delMemberPermission(memberId);
},
successToast: 'Success',
errorToast: 'Error',
successToast: '删除管理员成功',
errorToast: '删除管理员异常',
onSuccess: () => {
refetchMembers();
},
onError: () => {
refetchMembers();
}
});
return (
<Flex flexDirection={'column'} flex={'1'} h={['auto', '100%']} bg={'white'}>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
<MyBox h={'100%'} isLoading={isRemovingManager} bg={'white'}>
<Flex
mx={'5'}
flexDirection={'row'}
@ -73,7 +60,7 @@ function PermissionManage() {
px={'3'}
borderRadius={'sm'}
>
,
{TeamPermissionList['manage'].description}
</Box>
</Flex>
{userInfo?.team.role === 'owner' && (
@ -93,7 +80,7 @@ function PermissionManage() {
</Flex>
<Flex mt="4" mx="4">
{members.map((member) => {
if (hasManage(member.permission) && member.role !== TeamMemberRoleEnum.owner) {
if (member.permission.hasManagePer && !member.permission.isOwner) {
return (
<Tag key={member.memberName} mx={'2'} px="4" py="2" bg="myGray.100">
<Avatar src={member.avatar} w="20px" />
@ -106,6 +93,7 @@ function PermissionManage() {
w="16px"
color="myGray.500"
cursor="pointer"
_hover={{ color: 'red.600' }}
onClick={() => {
removeManager(member.tmbId);
}}
@ -116,7 +104,11 @@ function PermissionManage() {
}
})}
</Flex>
</Flex>
{isOpenAddManager && (
<AddManagerModal onClose={onCloseAddManager} onSuccess={onCloseAddManager} />
)}
</MyBox>
);
}

View File

@ -0,0 +1,102 @@
import React, { ReactNode, useState } from 'react';
import { createContext } from 'use-context-selector';
import type { EditTeamFormDataType } from './components/EditInfoModal';
import dynamic from 'next/dynamic';
import { useQuery } from '@tanstack/react-query';
import { getTeamList, getTeamMembers, putSwitchTeam } from '@/web/support/user/team/api';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
import type { TeamTmbItemType, TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
const EditInfoModal = dynamic(() => import('./components/EditInfoModal'));
type TeamModalContextType = {
myTeams: TeamTmbItemType[];
refetchTeams: () => void;
isLoading: boolean;
onSwitchTeam: (teamId: string) => void;
setEditTeamData: React.Dispatch<React.SetStateAction<EditTeamFormDataType | undefined>>;
members: TeamMemberItemType[];
refetchMembers: () => void;
};
export const TeamModalContext = createContext<TeamModalContextType>({
myTeams: [],
isLoading: false,
onSwitchTeam: function (teamId: string): void {
throw new Error('Function not implemented.');
},
setEditTeamData: function (value: React.SetStateAction<EditTeamFormDataType | undefined>): void {
throw new Error('Function not implemented.');
},
members: [],
refetchTeams: function (): void {
throw new Error('Function not implemented.');
},
refetchMembers: function (): void {
throw new Error('Function not implemented.');
}
});
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation();
const [editTeamData, setEditTeamData] = useState<EditTeamFormDataType>();
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeams
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
// member action
const {
data: members = [],
refetch: refetchMembers,
isLoading: loadingMembers
} = useQuery(['getMembers', userInfo?.team?.teamId], () => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers();
});
const { mutate: onSwitchTeam, isLoading: isSwitchingTeam } = useRequest({
mutationFn: async (teamId: string) => {
await putSwitchTeam(teamId);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
const isLoading = isLoadingTeams || isSwitchingTeam || loadingMembers;
const contextValue = {
myTeams,
refetchTeams,
isLoading,
onSwitchTeam,
// create | update team
setEditTeamData,
members,
refetchMembers
};
return (
<TeamModalContext.Provider value={contextValue}>
{children}
{!!editTeamData && (
<EditInfoModal
defaultData={editTeamData}
onClose={() => setEditTeamData(undefined)}
onSuccess={() => {
refetchTeams();
initUserInfo();
}}
/>
)}
</TeamModalContext.Provider>
);
};

View File

@ -1,130 +1,24 @@
import React, { useMemo, useState } from 'react';
import React from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import {
getTeamMembers,
delRemoveMember,
getTeamList,
delLeaveTeam,
putSwitchTeam
} from '@/web/support/user/team/api';
import { Box, useDisclosure } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import dynamic from 'next/dynamic';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useLoading } from '@fastgpt/web/hooks/useLoading';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { createContext } from 'use-context-selector';
import { createContext, useContextSelector } from 'use-context-selector';
import TeamList from './TeamList';
import TeamCard from './TeamCard';
import { FormDataType } from './EditModal';
import { TeamMemberStatusEnum } from '@fastgpt/global/support/user/team/constant';
import { setToken } from '@/web/support/user/auth';
import { TeamModalContext, TeamModalContextProvider } from './context';
const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('../TeamTagModal'));
export const TeamContext = createContext<{}>({} as any);
export const TeamContext = createContext<{
editTeamData?: FormDataType;
setEditTeamData: React.Dispatch<React.SetStateAction<any>>;
members: Awaited<ReturnType<typeof getTeamMembers>>;
myTeams: Awaited<ReturnType<typeof getTeamList>>;
refetchTeam: ReturnType<typeof useQuery>['refetch'];
onSwitchTeam: ReturnType<typeof useRequest>['mutate'];
refetchMembers: ReturnType<typeof useQuery>['refetch'];
openRemoveMember: ReturnType<typeof useConfirm>['openConfirm'];
onOpenInvite: ReturnType<typeof useDisclosure>['onOpen'];
onOpenTeamTagsAsync: ReturnType<typeof useDisclosure>['onOpen'];
onRemoveMember: ReturnType<typeof useRequest>['mutate'];
onLeaveTeam: ReturnType<typeof useRequest>['mutate'];
}>({} as any);
type Props = { onClose: () => void };
const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const TeamManageModal = ({ onClose }: Props) => {
const { Loading } = useLoading();
const { isLoading } = useContextSelector(TeamModalContext, (v) => v);
const { ConfirmModal: ConfirmRemoveMemberModal, openConfirm: openRemoveMember } = useConfirm();
const { userInfo, initUserInfo } = useUserStore();
const {
data: myTeams = [],
isFetching: isLoadingTeams,
refetch: refetchTeam
} = useQuery(['getTeams', userInfo?._id], () => getTeamList(TeamMemberStatusEnum.active));
const [editTeamData, setEditTeamData] = useState<FormDataType>();
const { isOpen: isOpenInvite, onOpen: onOpenInvite, onClose: onCloseInvite } = useDisclosure();
const {
isOpen: isOpenTeamTagsAsync,
onOpen: onOpenTeamTagsAsync,
onClose: onCloseTeamTagsAsync
} = useDisclosure();
// member action
const { data: members = [], refetch: refetchMembers } = useQuery(
['getMembers', userInfo?.team?.teamId],
() => {
if (!userInfo?.team?.teamId) return [];
return getTeamMembers();
}
);
const { mutate: onRemoveMember, isLoading: isLoadingRemoveMember } = useRequest({
mutationFn: delRemoveMember,
onSuccess() {
refetchMembers();
},
successToast: t('user.team.Remove Member Success'),
errorToast: t('user.team.Remove Member Failed')
});
const defaultTeam = useMemo(
() => myTeams.find((item) => item.defaultTeam) || myTeams[0],
[myTeams]
);
const { mutate: onSwitchTeam, isLoading: isSwitchTeam } = useRequest({
mutationFn: async (teamId: string) => {
const token = await putSwitchTeam(teamId);
token && setToken(token);
return initUserInfo();
},
errorToast: t('user.team.Switch Team Failed')
});
const { mutate: onLeaveTeam, isLoading: isLoadingLeaveTeam } = useRequest({
mutationFn: async (teamId?: string) => {
if (!teamId) return;
// change to personal team
// get members
onSwitchTeam(defaultTeam.teamId);
return delLeaveTeam(teamId);
},
onSuccess() {
refetchTeam();
},
errorToast: t('user.team.Leave Team Failed')
});
return !!userInfo?.team ? (
<TeamContext.Provider
value={{
myTeams: myTeams,
refetchTeam: refetchTeam,
onSwitchTeam: onSwitchTeam,
members: members,
refetchMembers: refetchMembers,
openRemoveMember: openRemoveMember,
onOpenInvite: onOpenInvite,
onOpenTeamTagsAsync: onOpenTeamTagsAsync,
onRemoveMember: onRemoveMember,
editTeamData: editTeamData,
setEditTeamData: setEditTeamData,
onLeaveTeam: onLeaveTeam
}}
>
return (
<>
<MyModal
isOpen
onClose={onClose}
@ -137,24 +31,23 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
>
<Box display={['block', 'flex']} flex={1} position={'relative'} overflow={'auto'}>
<TeamList />
<TeamCard />
<Loading
loading={isLoadingRemoveMember || isLoadingTeams || isLoadingLeaveTeam || isSwitchTeam}
fixed={false}
/>
<Box h={'100%'} flex={'1 0 0'}>
<TeamCard />
</Box>
<Loading loading={isLoading} fixed={false} />
</Box>
</MyModal>
{isOpenInvite && userInfo?.team?.teamId && (
<InviteModal
teamId={userInfo.team.teamId}
onClose={onCloseInvite}
onSuccess={refetchMembers}
/>
)}
{isOpenTeamTagsAsync && <TeamTagModal onClose={onCloseTeamTagsAsync} />}
<ConfirmRemoveMemberModal />
</TeamContext.Provider>
) : null;
</>
);
};
export default React.memo(TeamManageModal);
const Render = (props: Props) => {
const { userInfo } = useUserStore();
return !!userInfo?.team ? (
<TeamModalContextProvider>
<TeamManageModal {...props} />
</TeamModalContextProvider>
) : null;
};
export default React.memo(Render);

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Box, Button, Flex, Image, useDisclosure, useTheme } from '@chakra-ui/react';
import { Box, Button, Flex, Image, useDisclosure } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useTranslation } from 'next-i18next';
import MyTooltip from '@/components/MyTooltip';
@ -10,7 +10,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
const TeamManageModal = dynamic(() => import('../TeamManageModal'));
const TeamMenu = () => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
const { userInfo } = useUserStore();
@ -38,7 +37,7 @@ const TeamMenu = () => {
} else {
toast({
status: 'warning',
title: t('common.Business edition features')
title: t('common.system.Commercial version function')
});
}
}}

View File

@ -19,6 +19,7 @@ export type AppUpdateParams = {
chatConfig?: AppSchema['chatConfig'];
permission?: AppSchema['permission'];
teamTags?: AppSchema['teamTags'];
defaultPermission?: AppSchema['defaultPermission'];
};
export type PostPublishAppProps = {

View File

@ -265,7 +265,7 @@ const MyInfo = () => {
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && userInfo?.team?.canWrite && (
{feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer && (
<Button variant={'whitePrimary'} size={'sm'} ml={5} onClick={onOpenPayModal}>
{t('user.Pay')}
</Button>

View File

@ -105,7 +105,7 @@ const UsageTable = () => {
px={[3, 8]}
alignItems={['flex-end', 'center']}
>
{tmbList.length > 1 && userInfo?.team?.canWrite && (
{tmbList.length > 1 && userInfo?.team?.permission.hasWritePer && (
<Flex alignItems={'center'}>
<Box mr={2} flexShrink={0}>
{t('support.user.team.member')}

View File

@ -51,7 +51,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
}
]
: []),
...(feConfigs?.show_pay && userInfo?.team.canWrite
...(feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer
? [
{
icon: 'support/bill/payRecordLight',
@ -70,7 +70,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
}
]
: []),
...(userInfo?.team.canWrite
...(userInfo?.team?.permission.hasWritePer
? [
{
icon: 'support/outlink/apikeyLight',

View File

@ -3,11 +3,12 @@ import { jsonRes } from '@fastgpt/service/common/response';
import type { CreateAppParams } from '@/global/core/app/api.d';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { checkTeamAppLimit } from '@fastgpt/service/support/permission/teamLimit';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const {
@ -23,7 +24,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// 凭证校验
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
// 上限校验
await checkTeamAppLimit(teamId);

View File

@ -2,12 +2,13 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { NextAPI } from '@/service/middleware/entry';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { appId } = req.query as { appId: string };
@ -17,7 +18,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: 'owner' });
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
// 删除对应的聊天
await mongoSessionRun(async (session) => {

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@ -9,9 +10,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (!appId) {
throw new Error('参数错误');
}
// 凭证校验
const { app } = await authApp({ req, authToken: true, appId, per: 'w' });
const { app } = await authApp({ req, authToken: true, appId, per: WritePermissionVal });
return app;
}

View File

@ -5,9 +5,10 @@ import { AppLogsListItemType } from '@/types/app';
import { Types } from '@fastgpt/service/common/mongo';
import { addDays } from 'date-fns';
import type { GetAppChatLogsParams } from '@/global/core/api/appReq.d';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ChatItemCollectionName } from '@fastgpt/service/core/chat/chatItemSchema';
import { NextAPI } from '@/service/middleware/entry';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(
req: NextApiRequest,
@ -26,7 +27,7 @@ async function handler(
}
// 凭证校验
const { teamId } = await authApp({ req, authToken: true, appId, per: 'w' });
const { teamId } = await authApp({ req, authToken: true, appId, per: WritePermissionVal });
const where = {
teamId: new Types.ObjectId(teamId),

View File

@ -1,29 +1,63 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { mongoRPermission } from '@fastgpt/global/support/permission/utils';
import { AppListItemType } from '@fastgpt/global/core/app/type';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { NextAPI } from '@/service/middleware/entry';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import {
PerResourceTypeEnum,
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { AppPermission } from '@fastgpt/global/support/permission/app/controller';
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<AppListItemType[]> {
// 凭证校验
const { teamId, tmbId, teamOwner, role } = await authUserRole({ req, authToken: true });
// 根据 userId 获取模型信息
const myApps = await MongoApp.find(
{ ...mongoRPermission({ teamId, tmbId, role }) },
'_id avatar name intro tmbId permission'
).sort({
updateTime: -1
const {
teamId,
tmbId,
permission: tmbPer
} = await authUserPer({
req,
authToken: true,
per: ReadPermissionVal
});
return myApps.map((app) => ({
/* temp: get all apps and per */
const [myApps, rpList] = await Promise.all([
MongoApp.find({ teamId }, '_id avatar name intro tmbId defaultPermission')
.sort({
updateTime: -1
})
.lean(),
MongoResourcePermission.find({
resourceType: PerResourceTypeEnum.app,
teamId,
tmbId
}).lean()
]);
const filterApps = myApps
.map((app) => {
const perVal = rpList.find((item) => String(item.resourceId) === String(app._id))?.permission;
const Per = new AppPermission({
per: perVal ?? app.defaultPermission,
isOwner: String(app.tmbId) === tmbId || tmbPer.isOwner
});
return {
...app,
permission: Per
};
})
.filter((app) => app.permission.hasReadPer);
return filterApps.map((app) => ({
_id: app._id,
avatar: app.avatar,
name: app.name,
intro: app.intro,
isOwner: teamOwner || String(app.tmbId) === tmbId,
permission: app.permission
permission: app.permission,
defaultPermission: app.defaultPermission
}));
}

View File

@ -1,14 +1,29 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import type { AppUpdateParams } from '@/global/core/app/api';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import {
ManagePermissionVal,
WritePermissionVal,
OwnerPermissionVal
} from '@fastgpt/global/support/permission/constant';
/* 获取我的模型 */
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { name, avatar, type, intro, nodes, edges, chatConfig, permission, teamTags } =
req.body as AppUpdateParams;
const {
name,
avatar,
type,
intro,
nodes,
edges,
chatConfig,
permission,
teamTags,
defaultPermission
} = req.body as AppUpdateParams;
const { appId } = req.query as { appId: string };
if (!appId) {
@ -16,7 +31,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// 凭证校验
await authApp({ req, authToken: true, appId, per: permission ? 'owner' : 'w' });
if (permission) {
await authApp({ req, authToken: true, appId, per: OwnerPermissionVal });
} else if (defaultPermission) {
await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
} else {
await authApp({ req, authToken: true, appId, per: WritePermissionVal });
}
// format nodes data
// 1. dataset search limit, less than model quoteMaxToken
@ -33,6 +54,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
avatar,
intro,
permission,
defaultPermission,
...(teamTags && teamTags),
...(formatNodes && {
modules: formatNodes

View File

@ -1,12 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
type Response = {};
@ -14,7 +15,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
const { appId } = req.query as { appId: string };
const { nodes = [], edges = [], chatConfig, type } = req.body as PostPublishAppProps;
await authApp({ appId, req, per: 'w', authToken: true });
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const { nodes: formatNodes } = beforeUpdateAppFormat({ nodes });

View File

@ -1,12 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoAppVersion } from '@fastgpt/service/core/app/version/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostRevertAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
type Response = {};
@ -14,7 +15,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
const { appId } = req.query as { appId: string };
const { editNodes = [], editEdges = [], versionId } = req.body as PostRevertAppProps;
await authApp({ appId, req, per: 'w', authToken: true });
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
const version = await MongoAppVersion.findOne({
_id: versionId,

View File

@ -10,7 +10,7 @@ import type {
ChatItemValueItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
@ -18,6 +18,7 @@ import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
import { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
export type Props = {
history: ChatItemType[];
@ -61,7 +62,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
/* user auth */
const [_, { teamId, tmbId }] = await Promise.all([
authApp({ req, authToken: true, appId, per: 'r' }),
authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
authCert({
req,
authToken: true

View File

@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
import type { InitChatProps, InitChatResponse } from '@/global/core/chat/api.d';
@ -10,6 +10,7 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(
req: NextApiRequest,
@ -30,13 +31,13 @@ async function handler(
req,
authToken: true,
appId,
per: 'r'
per: ReadPermissionVal
}),
chatId ? MongoChat.findOne({ appId, chatId }) : undefined
]);
// auth chat permission
if (chat && !app.canWrite && String(tmbId) !== String(chat?.tmbId)) {
if (chat && !app.permission.hasManagePer && String(tmbId) !== String(chat?.tmbId)) {
throw new Error(ChatErrEnum.unAuthChat);
}

View File

@ -1,7 +1,8 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type createChatInputGuideQuery = {};
@ -19,7 +20,7 @@ async function handler(
res: ApiResponseType<any>
): Promise<createInputGuideResponse> {
const { appId, textList } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
await authApp({ req, appId, authToken: true, per: WritePermissionVal });
try {
const result = await MongoChatInputGuide.insertMany(

View File

@ -1,7 +1,8 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
export type deleteChatInputGuideQuery = {};
@ -14,7 +15,7 @@ async function handler(
res: ApiResponseType<any>
): Promise<deleteInputGuideResponse> {
const { appId, dataIdList } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
await authApp({ req, appId, authToken: true, per: ReadPermissionVal });
console.log(dataIdList);
await MongoChatInputGuide.deleteMany({
_id: { $in: dataIdList },

View File

@ -4,7 +4,8 @@ import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/t
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { ChatInputGuideSchemaType } from '@fastgpt/global/core/chat/inputGuide/type';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
export type ChatInputGuideProps = PaginationProps<{
appId: string;
@ -18,7 +19,7 @@ async function handler(
): Promise<ChatInputGuideResponse> {
const { appId, pageSize, current, searchKey } = req.query;
await authApp({ req, appId, authToken: true, per: 'r' });
await authApp({ req, appId, authToken: true, per: ReadPermissionVal });
const params = {
appId,

View File

@ -1,7 +1,8 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schema';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
export type updateChatInputGuideQuery = {};
@ -18,7 +19,7 @@ async function handler(
res: ApiResponseType<any>
): Promise<updateInputGuideResponse> {
const { appId, dataId, text } = req.body;
await authApp({ req, appId, authToken: true, per: 'r' });
await authApp({ req, appId, authToken: true, per: ReadPermissionVal });
await MongoChatInputGuide.findOneAndUpdate(
{

View File

@ -3,9 +3,10 @@ import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import type { DatasetSimpleItemType } from '@fastgpt/global/core/dataset/type.d';
import { mongoRPermission } from '@fastgpt/global/support/permission/utils';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
/* get all dataset by teamId or tmbId */
async function handler(
@ -13,10 +14,14 @@ async function handler(
res: NextApiResponse<any>
): Promise<DatasetSimpleItemType[]> {
// 凭证校验
const { teamId, tmbId, teamOwner, role } = await authUserRole({ req, authToken: true });
const { teamId, tmbId, permission } = await authUserPer({
req,
authToken: true,
per: ReadPermissionVal
});
const datasets = await MongoDataset.find({
...mongoRPermission({ teamId, tmbId, role }),
...mongoRPermission({ teamId, tmbId, permission }),
type: { $ne: DatasetTypeEnum.folder }
}).lean();

View File

@ -4,10 +4,11 @@ import { connectToDatabase } from '@/service/mongo';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import type { CreateDatasetParams } from '@/global/core/dataset/api.d';
import { createDefaultCollection } from '@fastgpt/service/core/dataset/collection/controller';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { getLLMModel, getVectorModel, getDatasetModel } from '@fastgpt/service/core/ai/model';
import { checkTeamDatasetLimit } from '@fastgpt/service/support/permission/teamLimit';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@ -22,7 +23,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
} = req.body as CreateDatasetParams;
// auth
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true, authApiKey: true });
const { teamId, tmbId } = await authUserPer({
req,
authToken: true,
authApiKey: true,
per: WritePermissionVal
});
// check model valid
const vectorModelStore = getVectorModel(vectorModel);

View File

@ -4,21 +4,23 @@ import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type.d';
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { MongoDataset } from '@fastgpt/service/core/dataset/schema';
import { mongoRPermission } from '@fastgpt/global/support/permission/utils';
import { authUserRole } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { getVectorModel } from '@fastgpt/service/core/ai/model';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { parentId, type } = req.query as { parentId?: string; type?: DatasetTypeEnum };
// 凭证校验
const { teamId, tmbId, teamOwner, role, canWrite } = await authUserRole({
const { teamId, tmbId, permission } = await authUserPer({
req,
authToken: true,
authApiKey: true
authApiKey: true,
per: ReadPermissionVal
});
const datasets = await MongoDataset.find({
...mongoRPermission({ teamId, tmbId, role }),
...mongoRPermission({ teamId, tmbId, permission }),
...(parentId !== undefined && { parentId: parentId || null }),
...(type && { type })
})
@ -34,8 +36,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
intro: item.intro,
type: item.type,
permission: item.permission,
canWrite,
isOwner: teamOwner || String(item.tmbId) === tmbId,
canWrite: permission.hasWritePer,
isOwner: permission.isOwner || String(item.tmbId) === tmbId,
vectorModel: getVectorModel(item.vectorModel)
}))
);

View File

@ -2,15 +2,16 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateOnePluginParams } from '@fastgpt/global/core/plugin/controller';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { httpApiSchema2Plugins } from '@fastgpt/global/core/plugin/httpPlugin/utils';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const body = req.body as CreateOnePluginParams;
// await checkTeamPluginLimit(teamId);

View File

@ -3,13 +3,14 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoPlugin } from '@fastgpt/service/core/plugin/schema';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { teamId } = await authUserNotVisitor({ req, authToken: true });
const { teamId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const { pluginId } = req.query as { pluginId: string };
if (!pluginId) {

View File

@ -1,13 +1,14 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { PostWorkflowDebugProps, PostWorkflowDebugResponse } from '@/global/core/workflow/api';
import { authPluginCrud } from '@fastgpt/service/support/permission/auth/plugin';
import { NextAPI } from '@/service/middleware/entry';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(
req: NextApiRequest,
@ -37,7 +38,7 @@ async function handler(
req,
authToken: true
}),
appId && authApp({ req, authToken: true, appId, per: 'r' }),
appId && authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
pluginId && authPluginCrud({ req, authToken: true, pluginId, per: 'r' })
]);

View File

@ -4,13 +4,15 @@ import { connectToDatabase } from '@/service/mongo';
import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema';
import { customAlphabet } from 'nanoid';
import type { EditApiKeyProps } from '@/global/support/openapi/api';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { getNanoid } from '@fastgpt/global/common/string/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId, name, limit } = req.body as EditApiKeyProps;
const { teamId, tmbId } = await authUserNotVisitor({ req, authToken: true });
const { teamId, tmbId } = await authUserPer({ req, authToken: true, per: WritePermissionVal });
const count = await MongoOpenApi.find({ tmbId, appId }).countDocuments();
@ -18,11 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('最多 10 组 API 秘钥');
}
const nanoid = customAlphabet(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
Math.floor(Math.random() * 14) + 52
);
const apiKey = `${global.systemEnv?.openapiPrefix || 'fastgpt'}-${nanoid()}`;
const nanoid = getNanoid(Math.floor(Math.random() * 14) + 52);
const apiKey = `${global.systemEnv?.openapiPrefix || 'fastgpt'}-${nanoid}`;
await MongoOpenApi.create({
teamId,

View File

@ -3,8 +3,9 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema';
import type { GetApiKeyProps } from '@/global/support/openapi/api';
import { authUserNotVisitor } from '@fastgpt/service/support/permission/auth/user';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -12,11 +13,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { appId } = req.query as GetApiKeyProps;
if (appId) {
const { tmbId, teamOwner } = await authApp({ req, authToken: true, appId, per: 'w' });
await authApp({
req,
authToken: true,
appId,
per: ManagePermissionVal
});
const findResponse = await MongoOpenApi.find({
appId,
...(!teamOwner && { tmbId })
appId
}).sort({ _id: -1 });
return jsonRes(res, {
@ -24,16 +29,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
const {
teamId,
tmbId,
isOwner: teamOwner
} = await authUserNotVisitor({ req, authToken: true });
const { teamId, tmbId, permission } = await authUserPer({
req,
authToken: true,
per: ManagePermissionVal
});
const findResponse = await MongoOpenApi.find({
appId,
teamId,
...(!teamOwner && { tmbId })
...(!permission.isOwner && { tmbId })
}).sort({ _id: -1 });
return jsonRes(res, {

View File

@ -2,10 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import type { OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { customAlphabet } from 'nanoid';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
/* create a shareChat */
@ -18,7 +19,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: PublishChannelEnum;
};
const { teamId, tmbId } = await authApp({ req, authToken: true, appId, per: 'w' });
const { teamId, tmbId } = await authApp({
req,
authToken: true,
appId,
per: WritePermissionVal
});
const shareId = nanoid();
await MongoOutLink.create({

View File

@ -2,7 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authOutLinkCrud } from '@fastgpt/service/support/permission/auth/outLink';
import { authOutLinkCrud } from '@fastgpt/service/support/permission/publish/authLink';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
/* delete a shareChat by shareChatId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -13,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: string;
};
await authOutLinkCrud({ req, outLinkId: id, authToken: true, per: 'owner' });
await authOutLinkCrud({ req, outLinkId: id, authToken: true, per: ManagePermissionVal });
await MongoOutLink.findByIdAndRemove(id);

View File

@ -2,7 +2,8 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
/* get shareChat list by appId */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -14,11 +15,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
type: string;
};
const { teamId, tmbId, isOwner } = await authApp({ req, authToken: true, appId, per: 'w' });
await authApp({
req,
authToken: true,
appId,
per: ManagePermissionVal
});
const data = await MongoOutLink.find({
appId,
...(isOwner ? { teamId } : { tmbId }),
type: type
}).sort({
_id: -1

View File

@ -3,7 +3,8 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import type { OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { authOutLinkCrud } from '@fastgpt/service/support/permission/auth/outLink';
import { authOutLinkCrud } from '@fastgpt/service/support/permission/publish/authLink';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
@ -15,7 +16,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('_id is required');
}
await authOutLinkCrud({ req, outLinkId: _id, authToken: true, per: 'owner' });
await authOutLinkCrud({ req, outLinkId: _id, authToken: true, per: ManagePermissionVal });
await MongoOutLink.findByIdAndUpdate(_id, {
name,

View File

@ -1,8 +1,9 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { UpdateTeamProps } from '@fastgpt/global/support/user/team/controller';
import { authTeamOwner } from '@fastgpt/service/support/permission/auth/user';
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
import { updateTeam } from '@fastgpt/service/support/user/team/controller';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
export type updateQuery = {};
@ -13,7 +14,7 @@ export type updateResponse = {};
async function handler(req: ApiRequestProps<updateBody, updateQuery>, res: ApiResponseType<any>) {
const body = req.body as UpdateTeamProps;
const { teamId } = await authTeamOwner({ req, authToken: true });
const { teamId } = await authUserPer({ req, authToken: true, per: ManagePermissionVal });
await updateTeam({ teamId, ...body });
}

View File

@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { authApp } from '@fastgpt/service/support/permission/auth/app';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { sseErrRes, jsonRes } from '@fastgpt/service/common/response';
import { addLog } from '@fastgpt/service/common/system/log';
@ -47,6 +47,7 @@ import { dispatchWorkFlowV1 } from '@fastgpt/service/core/workflow/dispatchV1';
import { setEntryEntries } from '@fastgpt/service/core/workflow/dispatchV1/utils';
import { NextAPI } from '@/service/middleware/entry';
import { getAppLatestVersion } from '@fastgpt/service/core/app/controller';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
type FastGptWebChatProps = {
chatId?: string; // undefined: nonuse history, '': new chat, 'xxxxx': use history
@ -482,17 +483,16 @@ const authHeaderRequest = async ({
if (!appId) {
return Promise.reject('appId is empty');
}
const { app, canWrite } = await authApp({
const { app, permission } = await authApp({
req,
authToken: true,
appId,
per: 'r'
per: ReadPermissionVal
});
return {
app,
canWrite: canWrite
canWrite: permission.hasReadPer
};
}
})();

View File

@ -7,8 +7,7 @@ import {
Input,
Textarea,
ModalFooter,
ModalBody,
Image
ModalBody
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
@ -19,39 +18,46 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import Avatar from '@/components/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import PermissionRadio from '@/components/support/permission/Radio';
import { useTranslation } from 'next-i18next';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { AppContext } from '@/web/core/app/context/appContext';
import MemberManager from '@/components/support/permission/MemberManager';
import {
postUpdateAppCollaborators,
deleteAppCollaborators,
getCollaboratorList
} from '@/web/core/app/api/collaborator';
import { useContextSelector } from 'use-context-selector';
import { AppContext } from '@/web/core/app/context/appContext';
import {
AppDefaultPermission,
AppPermissionList
} from '@fastgpt/global/support/permission/app/constant';
import { ReadPermissionVal, WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
const InfoModal = ({
defaultApp,
onClose,
onSuccess
}: {
defaultApp: AppSchema;
onClose: () => void;
onSuccess?: () => void;
}) => {
const InfoModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { toast } = useToast();
const { updateAppDetail } = useContextSelector(AppContext, (v) => v);
const { updateAppDetail, appDetail } = useContextSelector(AppContext, (v) => v);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const {
register,
setValue,
getValues,
formState: { errors },
handleSubmit
handleSubmit,
watch
} = useForm({
defaultValues: defaultApp
defaultValues: appDetail
});
const [refresh, setRefresh] = useState(false);
const defaultPermission = watch('defaultPermission');
const avatar = getValues('avatar');
// submit config
const { mutate: saveSubmitSuccess, isLoading: btnLoading } = useRequest({
@ -60,11 +66,10 @@ const InfoModal = ({
name: data.name,
avatar: data.avatar,
intro: data.intro,
permission: data.permission
defaultPermission: data.defaultPermission
});
},
onSuccess() {
onSuccess && onSuccess();
onClose();
toast({
title: t('common.Update Success'),
@ -108,7 +113,6 @@ const InfoModal = ({
maxH: 300
});
setValue('avatar', src);
setRefresh((state) => !state);
} catch (err: any) {
toast({
title: getErrText(err, t('common.error.Select avatar failed')),
@ -119,6 +123,20 @@ const InfoModal = ({
[setValue, t, toast]
);
const onUpdateCollaborators = async (tmbIds: string[], permission: PermissionValueType) => {
await postUpdateAppCollaborators({
tmbIds,
permission,
appId: appDetail._id
});
};
const onDelCollaborator = async (tmbId: string) => {
await deleteAppCollaborators({
appId: appDetail._id,
tmbId
});
};
return (
<MyModal
isOpen={true}
@ -130,7 +148,7 @@ const InfoModal = ({
<Box>{t('core.app.Name and avatar')}</Box>
<Flex mt={2} alignItems={'center'}>
<Avatar
src={getValues('avatar')}
src={avatar}
w={['26px', '34px']}
h={['26px', '34px']}
cursor={'pointer'}
@ -152,9 +170,6 @@ const InfoModal = ({
<Box mt={4} mb={1}>
{t('core.app.App intro')}
</Box>
{/* <Box color={'myGray.500'} mb={2} fontSize={'sm'}>
</Box> */}
<Textarea
rows={4}
maxLength={500}
@ -162,16 +177,32 @@ const InfoModal = ({
bg={'myWhite.600'}
{...register('intro')}
/>
<Box mt={4}>
<Box mb={1}>{t('user.Permission')}</Box>
<PermissionRadio
value={getValues('permission')}
onChange={(e) => {
setValue('permission', e);
setRefresh(!refresh);
}}
/>
</Box>
{/* role */}
{appDetail.permission.hasManagePer && (
<>
{' '}
<Box mt="4">
<Box>{t('permission.Default permission')}</Box>
<DefaultPermissionList
mt="2"
per={defaultPermission}
defaultPer={AppDefaultPermission}
readPer={ReadPermissionVal}
writePer={WritePermissionVal}
onChange={(v) => setValue('defaultPermission', v)}
/>
</Box>
<Box mt={6}>
<MemberManager
onGetCollaboratorList={() => getCollaboratorList(appDetail._id)}
permissionList={AppPermissionList}
onUpdateCollaborators={onUpdateCollaborators}
onDelOneCollaborator={onDelCollaborator}
/>
</Box>
</>
)}
</ModalBody>
<ModalFooter>

View File

@ -182,16 +182,17 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
label: t('common.Delete'),
icon: 'delete',
type: 'danger',
onClick: openConfirm(async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
})
onClick: () =>
openConfirm(async () => {
setIsLoading(true);
try {
await delShareChatById(item._id);
refetchShareChatList();
} catch (error) {
console.log(error);
}
setIsLoading(false);
})()
}
]}
/>

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Box, Flex, Button, IconButton } from '@chakra-ui/react';
import { Box, Flex, Button, IconButton, useDisclosure } from '@chakra-ui/react';
import { DragHandleIcon } from '@chakra-ui/icons';
import { useRequest } from '@fastgpt/web/hooks/useRequest';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
@ -19,17 +19,23 @@ import { AppContext } from '@/web/core/app/context/appContext';
import { useContextSelector } from 'use-context-selector';
const InfoModal = dynamic(() => import('../InfoModal'));
const AppCard = ({ appId }: { appId: string }) => {
const AppCard = () => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { toast } = useToast();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
const [settingAppInfo, setSettingAppInfo] = useState<AppSchema>();
const [TeamTagsSet, setTeamTagsSet] = useState<AppSchema>();
const {
isOpen: isOpenInfoEdit,
onOpen: onOpenInfoEdit,
onClose: onCloseInfoEdit
} = useDisclosure();
const { openConfirm: openConfirmDel, ConfirmModal: ConfirmDelModal } = useConfirm({
content: appT('Confirm Del App Tip'),
type: 'delete'
@ -58,7 +64,7 @@ const AppCard = ({ appId }: { appId: string }) => {
<Box px={4}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
<PermissionIconText permission={appDetail.permission} />
<PermissionIconText defaultPermission={appDetail.defaultPermission} />
</Box>
<Box color={'myGray.500'} fontSize={'sm'}>
AppId:{' '}
@ -83,7 +89,7 @@ const AppCard = ({ appId }: { appId: string }) => {
<Box ml={3} fontWeight={'bold'} fontSize={'lg'}>
{appDetail.name}
</Box>
{appDetail.isOwner && (
{appDetail.permission.isOwner && (
<IconButton
className="delete"
position={'absolute'}
@ -133,7 +139,7 @@ const AppCard = ({ appId }: { appId: string }) => {
>
{t('core.app.navbar.Publish')}
</Button>
{appDetail.canWrite && feConfigs?.show_team_chat && (
{appDetail.permission.hasWritePer && feConfigs?.show_team_chat && (
<Button
mr={3}
size={['sm', 'md']}
@ -144,12 +150,12 @@ const AppCard = ({ appId }: { appId: string }) => {
{t('common.Team Tags Set')}
</Button>
)}
{appDetail.isOwner && (
{appDetail.permission.hasManagePer && (
<Button
size={['sm', 'md']}
variant={'whitePrimary'}
leftIcon={<MyIcon name={'common/settingLight'} w={'16px'} />}
onClick={() => setSettingAppInfo(appDetail)}
onClick={onOpenInfoEdit}
>
{t('common.Setting')}
</Button>
@ -158,9 +164,7 @@ const AppCard = ({ appId }: { appId: string }) => {
</Box>
</Box>
<ConfirmDelModal />
{settingAppInfo && (
<InfoModal defaultApp={settingAppInfo} onClose={() => setSettingAppInfo(undefined)} />
)}
{isOpenInfoEdit && <InfoModal onClose={onCloseInfoEdit} />}
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
</>
);

View File

@ -56,7 +56,7 @@ const SimpleEdit = ({ appId }: { appId: string }) => {
pb={10}
overflow={'overlay'}
>
<AppCard appId={appId} />
<AppCard />
<Box mt={2}>
<EditForm editForm={editForm} divRef={divRef} isSticky={isSticky} />

View File

@ -59,24 +59,25 @@ const AppDetail = ({ appId, currentTab }: { appId: string; currentTab: TabEnum }
id: TabEnum.simpleEdit,
icon: 'common/overviewLight'
},
...(feConfigs?.hide_app_flow
? []
: [
{
label: t('core.app.navbar.Flow mode'),
id: TabEnum.adEdit,
icon: 'core/modules/flowLight'
}
]),
{
label: t('core.app.navbar.Publish app'),
id: TabEnum.publish,
icon: 'support/outlink/shareLight'
label: t('core.app.navbar.Flow mode'),
id: TabEnum.adEdit,
icon: 'core/modules/flowLight'
},
{ label: appT('Chat logs'), id: TabEnum.logs, icon: 'core/app/logsLight' },
...(appDetail.permission.hasManagePer
? [
{
label: t('core.app.navbar.Publish app'),
id: TabEnum.publish,
icon: 'support/outlink/shareLight'
},
{ label: appT('Chat logs'), id: TabEnum.logs, icon: 'core/app/logsLight' }
]
: []),
{ label: t('core.Start chat'), id: TabEnum.startChat, icon: 'core/chat/chatLight' }
],
[appT, feConfigs?.hide_app_flow, t]
[appDetail.permission.hasManagePer, appT, t]
);
const onCloseFlowEdit = useCallback(() => setCurrentTab(TabEnum.simpleEdit), [setCurrentTab]);

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import {
Box,
Flex,
@ -8,12 +8,8 @@ import {
Input,
Grid,
useTheme,
Card,
Text,
HStack,
Tag
Card
} from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
@ -136,52 +132,48 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
})}
/>
</Flex>
{!feConfigs?.hide_app_flow && (
<>
<Box mt={[4, 7]} mb={[0, 3]} color={'myGray.800'} fontWeight={'bold'}>
{t('core.app.Select app from template')}
</Box>
<Grid
userSelect={'none'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)']}
gridGap={[2, 4]}
<Box mt={[4, 7]} mb={[0, 3]} color={'myGray.800'} fontWeight={'bold'}>
{t('core.app.Select app from template')}
</Box>
<Grid
userSelect={'none'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)']}
gridGap={[2, 4]}
>
{appTemplates.map((item) => (
<Card
key={item.id}
border={theme.borders.base}
p={3}
borderRadius={'md'}
cursor={'pointer'}
boxShadow={'sm'}
{...(templateId === item.id
? {
bg: 'primary.50',
borderColor: 'primary.500'
}
: {
_hover: {
boxShadow: 'md'
}
})}
onClick={() => {
setValue('templateId', item.id);
}}
>
{appTemplates.map((item) => (
<Card
key={item.id}
border={theme.borders.base}
p={3}
borderRadius={'md'}
cursor={'pointer'}
boxShadow={'sm'}
{...(templateId === item.id
? {
bg: 'primary.50',
borderColor: 'primary.500'
}
: {
_hover: {
boxShadow: 'md'
}
})}
onClick={() => {
setValue('templateId', item.id);
}}
>
<Flex alignItems={'center'}>
<Avatar src={item.avatar} borderRadius={'md'} w={'20px'} />
<Box ml={3} fontWeight={'bold'}>
{t(item.name)}
</Box>
</Flex>
<Box fontSize={'sm'} mt={4}>
{t(item.intro)}
</Box>
</Card>
))}
</Grid>
</>
)}
<Flex alignItems={'center'}>
<Avatar src={item.avatar} borderRadius={'md'} w={'20px'} />
<Box ml={3} fontWeight={'bold'}>
{t(item.name)}
</Box>
</Flex>
<Box fontSize={'sm'} mt={4}>
{t(item.intro)}
</Box>
</Card>
))}
</Grid>
</ModalBody>
<ModalFooter>

Some files were not shown because too many files have changed in this diff Show More