From 2e179c4588fb2c94b1a4e9b5906c05abf6cbae52 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 3 Sep 2022 00:28:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/shared/utils/role-helper.ts | 23 +- client/web/package.json | 2 + .../GroupDetail/Role/tabs/permission.tsx | 25 +- .../modals/GroupDetail/Role/useRoleActions.ts | 10 +- client/web/src/plugin/common/reg.ts | 7 + client/web/tailchat.d.ts | 26 ++ package.json | 3 + pnpm-lock.yaml | 4 + server/models/group/group.ts | 26 +- server/packages/sdk/src/index.ts | 1 + server/packages/sdk/src/services/lib/call.ts | 18 +- .../sdk/src/services}/lib/role.ts | 0 .../services/subscribe.service.ts | 37 ++- .../plugins/com.msgbyte.github/src/index.tsx | 14 +- .../com.msgbyte.github/types/tailchat.d.ts | 301 +++++++++++++++++- server/services/core/group/group.service.ts | 2 +- server/services/core/group/invite.service.ts | 2 +- 17 files changed, 472 insertions(+), 29 deletions(-) rename server/{ => packages/sdk/src/services}/lib/role.ts (100%) diff --git a/client/shared/utils/role-helper.ts b/client/shared/utils/role-helper.ts index 570391a7..47a2e496 100644 --- a/client/shared/utils/role-helper.ts +++ b/client/shared/utils/role-helper.ts @@ -6,11 +6,27 @@ import { model, t } from '..'; */ export const AllPermission = Symbol('AllPermission'); -interface PermissionItem { +export interface PermissionItemType { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ key: string; + /** + * 权限点显示名称 + */ title: string; + /** + * 权限描述 + */ desc: string; + /** + * 是否默认开启 + */ default: boolean; + /** + * 是否依赖其他权限点 + */ required?: string[]; } @@ -29,10 +45,7 @@ export const PERMISSION = { }, }; -/** - * TODO: 后端校验还没做 - */ -export const permissionList: PermissionItem[] = [ +export const permissionList: PermissionItemType[] = [ { key: PERMISSION.core.message, title: t('发送消息'), diff --git a/client/web/package.json b/client/web/package.json index f705179d..9f16d030 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -70,6 +70,7 @@ "@types/copy-webpack-plugin": "^8.0.0", "@types/dts-generator": "^2.1.6", "@types/emoji-mart": "^3.0.8", + "@types/fs-extra": "^9.0.13", "@types/is-hotkey": "^0.1.5", "@types/jest": "^27.0.3", "@types/loadable__component": "^5.13.4", @@ -100,6 +101,7 @@ "esbuild-loader": "^2.13.1", "execa": "^5.1.1", "file-loader": "^6.2.0", + "fs-extra": "^10.0.0", "glob": "^7.2.0", "html-webpack-plugin": "^5.3.2", "jest": "^27.4.5", diff --git a/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx b/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx index 2a6ac5e3..ea71396b 100644 --- a/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx +++ b/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx @@ -1,9 +1,10 @@ import { AllPermission, permissionList } from 'tailchat-shared'; -import { Button } from 'antd'; +import { Button, Divider } from 'antd'; import React, { useCallback, useMemo } from 'react'; import { model, t } from 'tailchat-shared'; import { PermissionItem } from '../PermissionItem'; import { useModifyPermission } from '../useModifyPermission'; +import { pluginPermission } from '@/plugin/common'; interface RolePermissionProps { roleId: typeof AllPermission | string; @@ -63,6 +64,28 @@ export const RolePermission: React.FC = React.memo( onChange={(checked) => handleSwitchPermission(p.key, checked)} /> ))} + + {pluginPermission.length > 0 && ( + <> + {t('以下为插件权限')} + + {/* 权限详情 */} + {pluginPermission.map((p) => ( + editingPermission.includes(r)) + : undefined + } + checked={editingPermission.includes(p.key)} + onChange={(checked) => handleSwitchPermission(p.key, checked)} + /> + ))} + + )} ); } diff --git a/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts b/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts index 11e6f688..de8b8fcf 100644 --- a/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts +++ b/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts @@ -1,4 +1,8 @@ -import { AllPermission, getDefaultPermissionList } from 'tailchat-shared'; +import { + AllPermission, + getDefaultPermissionList, + showSuccessToasts, +} from 'tailchat-shared'; import { model, t, useAsyncRequest } from 'tailchat-shared'; export function useRoleActions( @@ -12,6 +16,7 @@ export function useRoleActions( t('新身份组'), getDefaultPermissionList() ); + showSuccessToasts(); }, [groupId]); const [{ loading: loading2 }, handleSavePermission] = useAsyncRequest( @@ -31,6 +36,8 @@ export function useRoleActions( permissions ); } + + showSuccessToasts(); }, [groupId, roleId] ); @@ -41,6 +48,7 @@ export function useRoleActions( throw new Error(t('无法修改所有人权限组的显示名称')); } await model.group.updateGroupRoleName(groupId, roleId, newRoleName); + showSuccessToasts(); }, [groupId, roleId] ); diff --git a/client/web/src/plugin/common/reg.ts b/client/web/src/plugin/common/reg.ts index 0f87d4ca..d6240148 100644 --- a/client/web/src/plugin/common/reg.ts +++ b/client/web/src/plugin/common/reg.ts @@ -5,6 +5,7 @@ import { ChatMessage, GroupPanel, regSocketEventListener, + PermissionItemType, } from 'tailchat-shared'; import type { MetaFormFieldMeta } from 'tailchat-design'; @@ -205,3 +206,9 @@ export interface DMPluginPanelActionProps extends BasePluginPanelActionProps { export const [pluginPanelActions, regPluginPanelAction] = buildRegList< GroupPluginPanelActionProps | DMPluginPanelActionProps >(); + +/** + * 注册插件权限 + */ +export const [pluginPermission, regPluginPermission] = + buildRegList(); diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index c03dd919..d2a277b3 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -179,6 +179,32 @@ declare module '@capital/common' { export const pluginPanelActions: any; export const regPluginPanelAction: any; + + export const pluginPermission: any; + + export const regPluginPermission: (permission: { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ + key: string; + /** + * 权限点显示名称 + */ + title: string; + /** + * 权限描述 + */ + desc: string; + /** + * 是否默认开启 + */ + default: boolean; + /** + * 是否依赖其他权限点 + */ + required?: string[]; + }) => void; } /** diff --git a/package.json b/package.json index bcbd0294..6ae9db79 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "build:web": "cd client/web && pnpm run build", "build:server": "cd server && pnpm run build && echo \"Install server side plugin:\" && pnpm run plugin:install com.msgbyte.tasks com.msgbyte.linkmeta com.msgbyte.github com.msgbyte.simplenotify && mkdir -p ./dist/public && cp -r ./public/plugins ./dist/public && cp ./public/registry.json ./dist/public", "postbuild": "cp -r client/web/dist/* server/dist/public", + "check:type": "concurrently npm:check:type:client npm:check:type:server", + "check:type:client": "cd client/web && tsc --noEmit", + "check:type:server": "cd server && tsc --noEmit", "preinstall": "npx only-allow pnpm", "lint:fix": "eslint --fix './**/*.{ts,tsx}'", "prepare": "husky install" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e0a27bc..46e5e06e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,6 +195,7 @@ importers: '@types/copy-webpack-plugin': ^8.0.0 '@types/dts-generator': ^2.1.6 '@types/emoji-mart': ^3.0.8 + '@types/fs-extra': ^9.0.13 '@types/is-hotkey': ^0.1.5 '@types/jest': ^27.0.3 '@types/loadable__component': ^5.13.4 @@ -231,6 +232,7 @@ importers: esbuild-loader: ^2.13.1 execa: ^5.1.1 file-loader: ^6.2.0 + fs-extra: ^10.0.0 glob: ^7.2.0 html-webpack-plugin: ^5.3.2 is-electron: ^2.2.1 @@ -335,6 +337,7 @@ importers: '@types/copy-webpack-plugin': 8.0.1_webpack-cli@4.10.0 '@types/dts-generator': 2.1.7 '@types/emoji-mart': 3.0.9 + '@types/fs-extra': 9.0.13 '@types/is-hotkey': 0.1.7 '@types/jest': 27.5.2 '@types/loadable__component': 5.13.4 @@ -365,6 +368,7 @@ importers: esbuild-loader: 2.19.0_webpack@5.73.0 execa: 5.1.1 file-loader: 6.2.0_webpack@5.73.0 + fs-extra: 10.1.0 glob: 7.2.3 html-webpack-plugin: 5.5.0_webpack@5.73.0 jest: 27.5.1_ts-node@10.9.1 diff --git a/server/models/group/group.ts b/server/models/group/group.ts index 438d6a75..d3f475aa 100644 --- a/server/models/group/group.ts +++ b/server/models/group/group.ts @@ -10,7 +10,7 @@ import { import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'; import _ from 'lodash'; import { Types } from 'mongoose'; -import { allPermission } from '../../lib/role'; +import { allPermission } from 'tailchat-server-sdk'; import { User } from '../user/user'; export enum GroupPanelType { @@ -253,11 +253,6 @@ export class Group extends TimeStamps implements Base { throw new Error('Not Found Group'); } - if (String(group.owner) === userId) { - // 群组管理者有所有权限 - return [...allPermission]; - } - const member = group.members.find( (member) => String(member.userId) === userId ); @@ -271,7 +266,24 @@ export class Group extends TimeStamps implements Base { return p?.permissions ?? []; }); - return _.union(...allRolesPermission, group.fallbackPermissions); // 权限取并集 + + if (String(group.owner) === userId) { + /** + * 群组管理者有所有权限 + * 这里是为了避免插件权限无法预先感知到的问题 + */ + + return _.uniq([ + ...allPermission, + ..._.flatten(allRolesPermission), + ...group.fallbackPermissions, + ]); + } else { + return _.uniq([ + ..._.flatten(allRolesPermission), + ...group.fallbackPermissions, + ]); + } } /** diff --git a/server/packages/sdk/src/index.ts b/server/packages/sdk/src/index.ts index 9a513a60..f903b034 100644 --- a/server/packages/sdk/src/index.ts +++ b/server/packages/sdk/src/index.ts @@ -13,6 +13,7 @@ export type { export { parseLanguageFromHead } from './services/lib/i18n/parser'; export { t } from './services/lib/i18n'; export * from './services/lib/errors'; +export { PERMISSION, allPermission } from './services/lib/role'; export { call } from './services/lib/call'; export { config, diff --git a/server/packages/sdk/src/services/lib/call.ts b/server/packages/sdk/src/services/lib/call.ts index 050a0c6f..a989747f 100644 --- a/server/packages/sdk/src/services/lib/call.ts +++ b/server/packages/sdk/src/services/lib/call.ts @@ -1,4 +1,10 @@ -import { GroupStruct, UserStruct, SYSTEM_USERID, TcContext } from '../../index'; +import { + GroupStruct, + UserStruct, + SYSTEM_USERID, + TcContext, + PERMISSION, +} from '../../index'; export function call(ctx: TcContext) { return { @@ -87,7 +93,15 @@ export function call(ctx: TcContext) { } ); - return permissions.map((p) => (userAllPermissions ?? []).includes(p)); + const hasOwnerPermission = userAllPermissions.includes( + PERMISSION.core.owner + ); + + return permissions.map((p) => + hasOwnerPermission + ? true // 如果有管理员权限。直接返回true + : (userAllPermissions ?? []).includes(p) + ); }, }; } diff --git a/server/lib/role.ts b/server/packages/sdk/src/services/lib/role.ts similarity index 100% rename from server/lib/role.ts rename to server/packages/sdk/src/services/lib/role.ts diff --git a/server/plugins/com.msgbyte.github/services/subscribe.service.ts b/server/plugins/com.msgbyte.github/services/subscribe.service.ts index 062319f5..8bddf1d6 100644 --- a/server/plugins/com.msgbyte.github/services/subscribe.service.ts +++ b/server/plugins/com.msgbyte.github/services/subscribe.service.ts @@ -3,10 +3,14 @@ import { TcPureContext, TcContext, TcDbService, + call, + NoPermissionError, } from 'tailchat-server-sdk'; import type { WebhookEvent } from '@octokit/webhooks-types'; import type { SubscribeDocument, SubscribeModel } from '../models/subscribe'; +const PERMISSION_MANAGE = 'plugin.com.msgbyte.github.subscribe.manage'; + /** * Github订阅服务 */ @@ -79,16 +83,19 @@ class GithubSubscribeService extends TcService { }> ) { const { groupId, textPanelId, repoName } = ctx.params; + const { userId, t } = ctx.meta; if (!groupId || !textPanelId || !repoName) { throw new Error('参数不全'); } - const isGroupOwner = await ctx.call('group.isGroupOwner', { + const [hasPermission] = await call(ctx).checkUserPermissions( groupId, - }); - if (isGroupOwner !== true) { - throw new Error('没有操作权限'); + userId, + [PERMISSION_MANAGE] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有操作权限')); } // TODO: 需要检查textPanelId是否合法 @@ -109,6 +116,16 @@ class GithubSubscribeService extends TcService { }> ) { const groupId = ctx.params.groupId; + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( + groupId, + userId, + [PERMISSION_MANAGE] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有查看权限')); + } const docs = await this.adapter.model .find({ @@ -129,11 +146,15 @@ class GithubSubscribeService extends TcService { }> ) { const { groupId, subscribeId } = ctx.params; - const isGroupOwner = await ctx.call('group.isGroupOwner', { + const { userId, t } = ctx.meta; + + const [hasPermission] = await call(ctx).checkUserPermissions( groupId, - }); - if (isGroupOwner !== true) { - throw new Error('没有操作权限'); + userId, + [PERMISSION_MANAGE] + ); + if (!hasPermission) { + throw new NoPermissionError(t('没有删除权限')); } await this.adapter.model.deleteOne({ diff --git a/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/src/index.tsx b/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/src/index.tsx index 83ae43a4..33533cff 100644 --- a/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/src/index.tsx +++ b/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/src/index.tsx @@ -1,4 +1,9 @@ -import { regCustomPanel, Loadable, regInspectService } from '@capital/common'; +import { + regCustomPanel, + Loadable, + regInspectService, + regPluginPermission, +} from '@capital/common'; import { Translate } from './translate'; regCustomPanel({ @@ -12,3 +17,10 @@ regInspectService({ name: 'plugin:com.msgbyte.github.subscribe', label: Translate.githubService, }); + +regPluginPermission({ + key: 'plugin.com.msgbyte.github.subscribe.manage', + title: 'Github 订阅管理', + desc: '允许管理Github订阅列表', + default: false, +}); diff --git a/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/types/tailchat.d.ts b/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/types/tailchat.d.ts index 49f524ae..d2a277b3 100644 --- a/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/types/tailchat.d.ts +++ b/server/plugins/com.msgbyte.github/web/plugins/com.msgbyte.github/types/tailchat.d.ts @@ -1,2 +1,299 @@ -declare module '@capital/common'; -declare module '@capital/component'; +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * 该文件由 Tailchat 自动生成 + * 用于插件的类型声明 + * 生成命令: pnpm run plugins:declaration:generate + */ + +/** + * Tailchat 通用 + */ +declare module '@capital/common' { + export const useGroupPanelParams: any; + + /** + * 打开模态框 + */ + export const openModal: ( + content: React.ReactNode, + + props?: { + /** + * 是否显示右上角的关闭按钮 + * @default false + */ + closable?: boolean; + + /** + * 遮罩层是否可关闭 + */ + maskClosable?: boolean; + + /** + * 关闭modal的回调 + */ + onCloseModal?: () => void; + } + ) => number; + + export const closeModal: any; + + export const ModalWrapper: any; + + export const useModalContext: any; + + export const openConfirmModal: any; + + export const openReconfirmModal: any; + + export const Loadable: any; + + export const getGlobalState: any; + + export const getJWTUserInfo: () => Promise<{ + _id?: string; + nickname?: string; + email?: string; + avatar?: string; + }>; + + export const dataUrlToFile: any; + + export const urlSearchStringify: any; + + export const urlSearchParse: any; + + export const appendUrlSearch: any; + + export const useGroupIdContext: any; + + export const getServiceUrl: () => string; + + export const getCachedUserInfo: ( + userId: string, + refetch?: boolean + ) => Promise<{ + _id: string; + email: string; + nickname: string; + discriminator: string; + avatar: string | null; + temporary: boolean; + }>; + + export const getCachedConverseInfo: any; + + export const localTrans: any; + + export const getLanguage: any; + + export const sharedEvent: any; + + export const useAsync: any; + + export const useAsyncFn: any; + + export const useAsyncRefresh: any; + + export const useAsyncRequest: any; + + export const uploadFile: any; + + export const showToasts: any; + + export const showErrorToasts: any; + + export const fetchAvailableServices: any; + + export const isValidStr: any; + + export const useGroupPanelInfo: any; + + export const sendMessage: any; + + export const useLocation: any; + + export const useHistory: any; + + export const createFastFormSchema: any; + + export const fieldSchema: any; + + export const useCurrentUserInfo: any; + + export const createPluginRequest: (pluginName: string) => { + get: (actionName: string, config?: any) => Promise; + post: (actionName: string, data?: any, config?: any) => Promise; + }; + + export const postRequest: any; + + export const pluginCustomPanel: any; + + export const regCustomPanel: any; + + export const pluginGroupPanel: any; + + export const regGroupPanel: any; + + export const messageInterpreter: any; + + export const regMessageInterpreter: any; + + export const getMessageRender: any; + + export const regMessageRender: any; + + export const getMessageTextDecorators: any; + + export const regMessageTextDecorators: any; + + export const ChatInputActionContextProps: any; + + export const pluginChatInputActions: any; + + export const regChatInputAction: any; + + export const regSocketEventListener: (item: { + eventName: string; + eventFn: (...args: any[]) => void; + }) => void; + + export const pluginColorScheme: any; + + export const regPluginColorScheme: any; + + export const pluginInspectServices: any; + + export const regInspectService: any; + + export const pluginMessageExtraParsers: any; + + export const regMessageExtraParser: any; + + export const pluginRootRoute: any; + + export const regPluginRootRoute: any; + + export const pluginPanelActions: any; + + export const regPluginPanelAction: any; + + export const pluginPermission: any; + + export const regPluginPermission: (permission: { + /** + * 权限唯一key, 用于写入数据库 + * 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage + */ + key: string; + /** + * 权限点显示名称 + */ + title: string; + /** + * 权限描述 + */ + desc: string; + /** + * 是否默认开启 + */ + default: boolean; + /** + * 是否依赖其他权限点 + */ + required?: string[]; + }) => void; +} + +/** + * Tailchat 组件 + */ +declare module '@capital/component' { + export const Button: any; + + export const Checkbox: any; + + export const Input: any; + + export const Divider: any; + + export const Space: any; + + export const Menu: any; + + export const Table: any; + + export const Switch: any; + + export const Tooltip: any; + + /** + * @link https://ant.design/components/notification-cn/ + */ + export const notification: any; + + export const Avatar: any; + + export const SensitiveText: React.FC<{ className?: string; text: string }>; + + export const TextArea: any; + + export const Image: any; + + export const Icon: any; + + export const IconBtn: any; + + export const PillTabs: any; + + export const PillTabPane: any; + + export const LoadingSpinner: any; + + export const WebFastForm: any; + + export const WebMetaForm: any; + + export const createMetaFormSchema: any; + + export const metaFormFieldSchema: any; + + export const FullModalField: any; + + export const DefaultFullModalInputEditorRender: any; + + export const DefaultFullModalTextAreaEditorRender: any; + + export const openModal: any; + + export const closeModal: any; + + export const ModalWrapper: any; + + export const useModalContext: any; + + export const openConfirmModal: any; + + export const openReconfirmModal: any; + + export const Loading: any; + + export const SidebarView: any; + + export const GroupPanelSelector: any; + + export const Emoji: any; + + export const PortalAdd: any; + + export const PortalRemove: any; + + export const ErrorBoundary: any; + + export const UserName: React.FC<{ + userId: string; + className?: string; + }>; +} diff --git a/server/services/core/group/group.service.ts b/server/services/core/group/group.service.ts index ac58fa1c..36426ac0 100644 --- a/server/services/core/group/group.service.ts +++ b/server/services/core/group/group.service.ts @@ -18,9 +18,9 @@ import { DataNotFoundError, EntityError, NoPermissionError, + PERMISSION, } from 'tailchat-server-sdk'; import moment from 'moment'; -import { PERMISSION } from '../../../lib/role'; interface GroupService extends TcService, diff --git a/server/services/core/group/invite.service.ts b/server/services/core/group/invite.service.ts index 9ddb9534..a83d2187 100644 --- a/server/services/core/group/invite.service.ts +++ b/server/services/core/group/invite.service.ts @@ -11,8 +11,8 @@ import { PureContext, call, NoPermissionError, + PERMISSION, } from 'tailchat-server-sdk'; -import { PERMISSION } from '../../../lib/role'; interface GroupService extends TcService,