feat: gate permission

This commit is contained in:
archer 2025-05-30 16:04:33 +08:00
parent e74ab643fe
commit 81202c53a8
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
6 changed files with 204 additions and 55 deletions

View File

@ -1,9 +1,18 @@
import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant'; import { NullPermission, PermissionKeyEnum, PermissionList } from '../constant';
import { type PermissionListType } from '../type'; import { type PermissionListType } from '../type';
import { i18nT } from '../../../../web/i18n/utils'; import { i18nT } from '../../../../web/i18n/utils';
export enum AppPermissionKeyEnum {}
export enum AppPermissionKeyEnum { export enum AppPermissionKeyEnum {
log = 'log' log = 'log',
quickGate = 'quickGate',
featuredGate = 'featuredGate'
} }
export const AppLogPermission = 0b100000;
export const GateQuickAppPermission = 0b001100;
export const GateFeaturedAppPermission = 0b010100;
export const AppPermissionList: PermissionListType<AppPermissionKeyEnum> = { export const AppPermissionList: PermissionListType<AppPermissionKeyEnum> = {
[PermissionKeyEnum.read]: { [PermissionKeyEnum.read]: {
...PermissionList[PermissionKeyEnum.read], ...PermissionList[PermissionKeyEnum.read],
@ -15,14 +24,26 @@ export const AppPermissionList: PermissionListType<AppPermissionKeyEnum> = {
}, },
[PermissionKeyEnum.manage]: { [PermissionKeyEnum.manage]: {
...PermissionList[PermissionKeyEnum.manage], ...PermissionList[PermissionKeyEnum.manage],
value: 0b1111, value: 0b111111,
description: i18nT('app:permission.des.manage') description: i18nT('app:permission.des.manage')
}, },
[AppPermissionKeyEnum.log]: { [AppPermissionKeyEnum.log]: {
name: i18nT('app:permission.name.log'), name: i18nT('app:permission.name.log'),
value: 0b1000, value: AppLogPermission,
checkBoxType: 'multiple', checkBoxType: 'multiple',
description: i18nT('app:permission.des.log') description: i18nT('app:permission.des.log')
},
[AppPermissionKeyEnum.quickGate]: {
name: '门户快捷应用权限',
description: '',
value: GateQuickAppPermission,
checkBoxType: 'multiple' // TODO: 加个隐藏选项
},
[AppPermissionKeyEnum.featuredGate]: {
name: '门户推荐应用权限',
description: '',
value: GateFeaturedAppPermission,
checkBoxType: 'multiple' // TODO: 加个隐藏选项
} }
}; };

View File

@ -1,5 +1,86 @@
import { MongoTeamGate, gateCollectionName } from './schema'; import { MongoTeamGate } from './schema';
import { Types } from '../../../../common/mongo'; import { Types } from '../../../../common/mongo';
import type { ClientSession } from '../../../../common/mongo';
import { mongoSessionRun } from '../../../../common/mongo/sessionRun';
import {
GateFeaturedAppPermission,
GateQuickAppPermission
} from '@fastgpt/global/support/permission/app/constant';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { MongoMemberGroupModel } from '../../../permission/memberGroup/memberGroupSchema';
import { MongoResourcePermission } from '../../../permission/schema';
export const addGatePermission = async ({
teamId,
appId,
per,
session
}: {
teamId: string;
appId: string;
per: number;
session?: ClientSession;
}) => {
// 1. 先找全员组
const teamGroup = await MongoMemberGroupModel.findOne({
teamId,
name: DefaultGroupName
});
if (!teamGroup) {
return Promise.reject('找不到全员组');
}
// 2. 加权限
await MongoResourcePermission.updateOne(
{
teamId,
groupId: teamGroup?._id,
resourceType: PerResourceTypeEnum.app,
resourceId: appId
},
{
permission: per
},
{
session,
upsert: true
}
);
};
export const removeGatePermission = async ({
teamId,
appId,
per,
session
}: {
teamId: string;
appId: string;
per: number;
session?: ClientSession;
}) => {
// 1. 先找全员组
const teamGroup = await MongoMemberGroupModel.findOne({
teamId,
name: DefaultGroupName
});
if (!teamGroup) {
return Promise.reject('找不到全员组');
}
await MongoResourcePermission.deleteOne(
{
teamId,
groupId: teamGroup?._id,
resourceType: PerResourceTypeEnum.app,
resourceId: appId,
permission: per
},
{
session
}
);
};
/** /**
* *
@ -133,8 +214,20 @@ export const addFeaturedApp = async ({ teamId, appId }: { teamId: string; appId:
/** /**
* *
*/ */
export const removeFeaturedApp = async ({ teamId, appId }: { teamId: string; appId: string }) => { export const removeFeaturedApp = async ({
await MongoTeamGate.updateOne({ teamId }, { $pull: { featuredApps: new Types.ObjectId(appId) } }); teamId,
appId,
session
}: {
teamId: string;
appId: string;
session?: ClientSession;
}) => {
await MongoTeamGate.updateOne(
{ teamId },
{ $pull: { featuredApps: new Types.ObjectId(appId) } },
{ session }
);
return MongoTeamGate.findOne({ teamId }).lean(); return MongoTeamGate.findOne({ teamId }).lean();
}; };
@ -252,7 +345,34 @@ export const batchUpdateFeaturedApps = async (
return true; return true;
} }
await MongoTeamGate.bulkWrite(operations); const teamId = updates[0]?.teamId;
const gateConfig = await MongoTeamGate.findOne({ teamId });
if (!gateConfig) return Promise.reject('无 gate 配置');
const updatedAppId = updates[0].featuredApps;
const deleteAppId = gateConfig.featuredApps.filter((id) => !updatedAppId.includes(id));
await mongoSessionRun(async (session) => {
await MongoTeamGate.bulkWrite(operations, { session });
for (const id of deleteAppId) {
await removeGatePermission({
teamId,
appId: id,
per: GateFeaturedAppPermission,
session
});
}
for (const id of updatedAppId) {
await addGatePermission({
teamId,
appId: id,
per: GateFeaturedAppPermission,
session
});
}
});
return true; return true;
}; };
@ -291,10 +411,12 @@ export const batchUpdateToolsOrder = async (
*/ */
export const batchDeleteFeaturedApps = async ({ export const batchDeleteFeaturedApps = async ({
teamId, teamId,
appIds appIds,
session
}: { }: {
teamId: string; teamId: string;
appIds: string[]; appIds: string[];
session?: ClientSession;
}) => { }) => {
if (!appIds || appIds.length === 0) { if (!appIds || appIds.length === 0) {
return false; return false;
@ -302,7 +424,10 @@ export const batchDeleteFeaturedApps = async ({
await MongoTeamGate.updateOne( await MongoTeamGate.updateOne(
{ teamId }, { teamId },
{ $pull: { featuredApps: { $in: appIds.map((id) => new Types.ObjectId(id)) } } } { $pull: { featuredApps: { $in: appIds.map((id) => new Types.ObjectId(id)) } } },
{
session
}
); );
return true; return true;
}; };
@ -384,31 +509,39 @@ export const moveQuickAppToPosition = async ({
/** /**
* *
*/ */
export const batchUpdateQuickApps = async ( export const batchUpdateQuickApps = async (teamId: string, quickApps: string[]) => {
updates: { const gateConfig = await MongoTeamGate.findOne({ teamId });
teamId: string; if (!gateConfig) {
quickApps: string[]; return false;
}[]
) => {
const operations = updates.map((update) => {
const { teamId, quickApps } = update;
// 将字符串数组转换为 ObjectId 数组
const objectIdArray = quickApps.map((id) => new Types.ObjectId(id));
return {
updateOne: {
filter: { teamId },
update: { $set: { quickApps: objectIdArray } },
upsert: true
}
};
});
if (operations.length === 0) {
return true;
} }
await MongoTeamGate.bulkWrite(operations); // 计算删除的appId
return true; const deleteAppIds = gateConfig.quickApps.filter((id) => !quickApps.includes(id.toString()));
return mongoSessionRun(async (session) => {
// 1. 删除权限
for (const id of deleteAppIds) {
await removeGatePermission({
teamId,
appId: id,
per: GateQuickAppPermission,
session
});
}
// 加权限
for (const id of quickApps) {
await addGatePermission({
teamId,
appId: id,
per: GateQuickAppPermission,
session
});
}
gateConfig.quickApps = quickApps;
await gateConfig.save({ session });
return true;
});
}; };
/** /**

View File

@ -62,11 +62,15 @@ const AccountContainer = ({
label: t('account:usage_records'), label: t('account:usage_records'),
value: TabEnum.usage value: TabEnum.usage
}, },
{ ...(userInfo?.team?.permission.hasManagePer
icon: 'support/gate/gateLight', ? [
label: t('account:gateways'), {
value: TabEnum.gateway icon: 'support/gate/gateLight',
} label: t('account:gateways'),
value: TabEnum.gateway
}
]
: [])
] ]
: []), : []),
...(feConfigs?.show_pay && userInfo?.team?.permission.hasManagePer ...(feConfigs?.show_pay && userInfo?.team?.permission.hasManagePer

View File

@ -338,7 +338,8 @@ const AppTable = () => {
); );
const { openConfirm: openConfirmDel, ConfirmModal: DelConfirmModal } = useConfirm({ const { openConfirm: openConfirmDel, ConfirmModal: DelConfirmModal } = useConfirm({
type: 'delete' type: 'delete',
title: '确认删除该应用?'
}); });
const { runAsync: onDeleteApp } = useRequest2(delAppById, { const { runAsync: onDeleteApp } = useRequest2(delAppById, {
@ -487,12 +488,11 @@ const AppTable = () => {
flexDirection={{ base: 'column', md: 'row' }} flexDirection={{ base: 'column', md: 'row' }}
alignItems={{ base: 'stretch', md: 'center' }} alignItems={{ base: 'stretch', md: 'center' }}
> >
<Flex flex={1} gap={4}> <Flex gap={4}>
<SearchInput <SearchInput
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={t('app:search_app')} placeholder={t('app:search_app')}
flex={1}
/> />
<Box w="200px"> <Box w="200px">
<Menu closeOnSelect={false}> <Menu closeOnSelect={false}>

View File

@ -158,12 +158,7 @@ const CopyrightTable = ({
{t('account_gate:gate_logo')} {t('account_gate:gate_logo')}
</Text> </Text>
<Flex <Flex gap={{ base: 4, md: 8 }} alignItems="center" justifyContent="flex-start">
gap={{ base: 4, md: 8 }}
alignItems="center"
justifyContent="flex-start"
flexDirection={{ base: 'column', md: 'row' }}
>
{/* 左侧 Banner 显示 - 带文字 */} {/* 左侧 Banner 显示 - 带文字 */}
<Flex direction="column" gap={2} alignItems="center"> <Flex direction="column" gap={2} alignItems="center">
<Box <Box
@ -369,8 +364,8 @@ const CopyrightTable = ({
<LogoFile <LogoFile
onSelect={(e: File[]) => onSelect={(e: File[]) =>
onSelectLogoImage(e, { onSelectLogoImage(e, {
maxH: 300, maxH: 3000,
maxW: 300, maxW: 3000,
callback: (e: string) => { callback: (e: string) => {
setValue('logo', e); setValue('logo', e);
handleGateLogoChange(e); handleGateLogoChange(e);
@ -382,8 +377,8 @@ const CopyrightTable = ({
<BannerFile <BannerFile
onSelect={(e: File[]) => onSelect={(e: File[]) =>
onSelectBannerImage(e, { onSelectBannerImage(e, {
maxH: 300, maxH: 3000,
maxW: 300, maxW: 3000,
callback: (e: string) => { callback: (e: string) => {
setValue('banner', e); setValue('banner', e);
handleGateBannerChange(e); handleGateBannerChange(e);

View File

@ -35,9 +35,7 @@ const GatewayConfig = () => {
//从 appForm 中获取 selectedTools的 id 组成 string 数组 //从 appForm 中获取 selectedTools的 id 组成 string 数组
//gateConfig?.tools 改成 //gateConfig?.tools 改成
const [copyRightConfig, setCopyRightConfig] = useState< const [copyRightConfig, setCopyRightConfig] = useState<getGateConfigCopyRightResponse>();
getGateConfigCopyRightResponse | undefined
>(undefined);
const [tab, setTab] = useState<TabType>('home'); const [tab, setTab] = useState<TabType>('home');
const [isLoadingApps, setIsLoadingApps] = useState(true); const [isLoadingApps, setIsLoadingApps] = useState(true);
const [gateApps, setGateApps] = useState<AppListItemType[]>([]); const [gateApps, setGateApps] = useState<AppListItemType[]>([]);
@ -247,8 +245,6 @@ const GatewayConfig = () => {
}, [ }, [
gateConfig, gateConfig,
copyRightConfig, copyRightConfig,
isLoadingApps,
gateApps,
Tab, Tab,
isAppTab, isAppTab,
tab, tab,