feat: 增加插件系统权限管理

pull/49/head
moonrailgun 3 years ago
parent 8fd124265c
commit 2e179c4588

@ -6,11 +6,27 @@ import { model, t } from '..';
*/ */
export const AllPermission = Symbol('AllPermission'); export const AllPermission = Symbol('AllPermission');
interface PermissionItem { export interface PermissionItemType {
/**
* key,
* , : plugin.com.msgbyte.github.manage
*/
key: string; key: string;
/**
*
*/
title: string; title: string;
/**
*
*/
desc: string; desc: string;
/**
*
*/
default: boolean; default: boolean;
/**
*
*/
required?: string[]; required?: string[];
} }
@ -29,10 +45,7 @@ export const PERMISSION = {
}, },
}; };
/** export const permissionList: PermissionItemType[] = [
* TODO:
*/
export const permissionList: PermissionItem[] = [
{ {
key: PERMISSION.core.message, key: PERMISSION.core.message,
title: t('发送消息'), title: t('发送消息'),

@ -70,6 +70,7 @@
"@types/copy-webpack-plugin": "^8.0.0", "@types/copy-webpack-plugin": "^8.0.0",
"@types/dts-generator": "^2.1.6", "@types/dts-generator": "^2.1.6",
"@types/emoji-mart": "^3.0.8", "@types/emoji-mart": "^3.0.8",
"@types/fs-extra": "^9.0.13",
"@types/is-hotkey": "^0.1.5", "@types/is-hotkey": "^0.1.5",
"@types/jest": "^27.0.3", "@types/jest": "^27.0.3",
"@types/loadable__component": "^5.13.4", "@types/loadable__component": "^5.13.4",
@ -100,6 +101,7 @@
"esbuild-loader": "^2.13.1", "esbuild-loader": "^2.13.1",
"execa": "^5.1.1", "execa": "^5.1.1",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"glob": "^7.2.0", "glob": "^7.2.0",
"html-webpack-plugin": "^5.3.2", "html-webpack-plugin": "^5.3.2",
"jest": "^27.4.5", "jest": "^27.4.5",

@ -1,9 +1,10 @@
import { AllPermission, permissionList } from 'tailchat-shared'; import { AllPermission, permissionList } from 'tailchat-shared';
import { Button } from 'antd'; import { Button, Divider } from 'antd';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { model, t } from 'tailchat-shared'; import { model, t } from 'tailchat-shared';
import { PermissionItem } from '../PermissionItem'; import { PermissionItem } from '../PermissionItem';
import { useModifyPermission } from '../useModifyPermission'; import { useModifyPermission } from '../useModifyPermission';
import { pluginPermission } from '@/plugin/common';
interface RolePermissionProps { interface RolePermissionProps {
roleId: typeof AllPermission | string; roleId: typeof AllPermission | string;
@ -63,6 +64,28 @@ export const RolePermission: React.FC<RolePermissionProps> = React.memo(
onChange={(checked) => handleSwitchPermission(p.key, checked)} onChange={(checked) => handleSwitchPermission(p.key, checked)}
/> />
))} ))}
{pluginPermission.length > 0 && (
<>
<Divider>{t('以下为插件权限')}</Divider>
{/* 权限详情 */}
{pluginPermission.map((p) => (
<PermissionItem
key={p.key}
title={p.title}
desc={p.desc}
disabled={
p.required
? !p.required.every((r) => editingPermission.includes(r))
: undefined
}
checked={editingPermission.includes(p.key)}
onChange={(checked) => handleSwitchPermission(p.key, checked)}
/>
))}
</>
)}
</div> </div>
); );
} }

@ -1,4 +1,8 @@
import { AllPermission, getDefaultPermissionList } from 'tailchat-shared'; import {
AllPermission,
getDefaultPermissionList,
showSuccessToasts,
} from 'tailchat-shared';
import { model, t, useAsyncRequest } from 'tailchat-shared'; import { model, t, useAsyncRequest } from 'tailchat-shared';
export function useRoleActions( export function useRoleActions(
@ -12,6 +16,7 @@ export function useRoleActions(
t('新身份组'), t('新身份组'),
getDefaultPermissionList() getDefaultPermissionList()
); );
showSuccessToasts();
}, [groupId]); }, [groupId]);
const [{ loading: loading2 }, handleSavePermission] = useAsyncRequest( const [{ loading: loading2 }, handleSavePermission] = useAsyncRequest(
@ -31,6 +36,8 @@ export function useRoleActions(
permissions permissions
); );
} }
showSuccessToasts();
}, },
[groupId, roleId] [groupId, roleId]
); );
@ -41,6 +48,7 @@ export function useRoleActions(
throw new Error(t('无法修改所有人权限组的显示名称')); throw new Error(t('无法修改所有人权限组的显示名称'));
} }
await model.group.updateGroupRoleName(groupId, roleId, newRoleName); await model.group.updateGroupRoleName(groupId, roleId, newRoleName);
showSuccessToasts();
}, },
[groupId, roleId] [groupId, roleId]
); );

@ -5,6 +5,7 @@ import {
ChatMessage, ChatMessage,
GroupPanel, GroupPanel,
regSocketEventListener, regSocketEventListener,
PermissionItemType,
} from 'tailchat-shared'; } from 'tailchat-shared';
import type { MetaFormFieldMeta } from 'tailchat-design'; import type { MetaFormFieldMeta } from 'tailchat-design';
@ -205,3 +206,9 @@ export interface DMPluginPanelActionProps extends BasePluginPanelActionProps {
export const [pluginPanelActions, regPluginPanelAction] = buildRegList< export const [pluginPanelActions, regPluginPanelAction] = buildRegList<
GroupPluginPanelActionProps | DMPluginPanelActionProps GroupPluginPanelActionProps | DMPluginPanelActionProps
>(); >();
/**
*
*/
export const [pluginPermission, regPluginPermission] =
buildRegList<PermissionItemType>();

@ -179,6 +179,32 @@ declare module '@capital/common' {
export const pluginPanelActions: any; export const pluginPanelActions: any;
export const regPluginPanelAction: 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;
} }
/** /**

@ -12,6 +12,9 @@
"build:web": "cd client/web && pnpm run build", "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", "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", "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", "preinstall": "npx only-allow pnpm",
"lint:fix": "eslint --fix './**/*.{ts,tsx}'", "lint:fix": "eslint --fix './**/*.{ts,tsx}'",
"prepare": "husky install" "prepare": "husky install"

@ -195,6 +195,7 @@ importers:
'@types/copy-webpack-plugin': ^8.0.0 '@types/copy-webpack-plugin': ^8.0.0
'@types/dts-generator': ^2.1.6 '@types/dts-generator': ^2.1.6
'@types/emoji-mart': ^3.0.8 '@types/emoji-mart': ^3.0.8
'@types/fs-extra': ^9.0.13
'@types/is-hotkey': ^0.1.5 '@types/is-hotkey': ^0.1.5
'@types/jest': ^27.0.3 '@types/jest': ^27.0.3
'@types/loadable__component': ^5.13.4 '@types/loadable__component': ^5.13.4
@ -231,6 +232,7 @@ importers:
esbuild-loader: ^2.13.1 esbuild-loader: ^2.13.1
execa: ^5.1.1 execa: ^5.1.1
file-loader: ^6.2.0 file-loader: ^6.2.0
fs-extra: ^10.0.0
glob: ^7.2.0 glob: ^7.2.0
html-webpack-plugin: ^5.3.2 html-webpack-plugin: ^5.3.2
is-electron: ^2.2.1 is-electron: ^2.2.1
@ -335,6 +337,7 @@ importers:
'@types/copy-webpack-plugin': 8.0.1_webpack-cli@4.10.0 '@types/copy-webpack-plugin': 8.0.1_webpack-cli@4.10.0
'@types/dts-generator': 2.1.7 '@types/dts-generator': 2.1.7
'@types/emoji-mart': 3.0.9 '@types/emoji-mart': 3.0.9
'@types/fs-extra': 9.0.13
'@types/is-hotkey': 0.1.7 '@types/is-hotkey': 0.1.7
'@types/jest': 27.5.2 '@types/jest': 27.5.2
'@types/loadable__component': 5.13.4 '@types/loadable__component': 5.13.4
@ -365,6 +368,7 @@ importers:
esbuild-loader: 2.19.0_webpack@5.73.0 esbuild-loader: 2.19.0_webpack@5.73.0
execa: 5.1.1 execa: 5.1.1
file-loader: 6.2.0_webpack@5.73.0 file-loader: 6.2.0_webpack@5.73.0
fs-extra: 10.1.0
glob: 7.2.3 glob: 7.2.3
html-webpack-plugin: 5.5.0_webpack@5.73.0 html-webpack-plugin: 5.5.0_webpack@5.73.0
jest: 27.5.1_ts-node@10.9.1 jest: 27.5.1_ts-node@10.9.1

@ -10,7 +10,7 @@ import {
import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses'; import { Base, TimeStamps } from '@typegoose/typegoose/lib/defaultClasses';
import _ from 'lodash'; import _ from 'lodash';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { allPermission } from '../../lib/role'; import { allPermission } from 'tailchat-server-sdk';
import { User } from '../user/user'; import { User } from '../user/user';
export enum GroupPanelType { export enum GroupPanelType {
@ -253,11 +253,6 @@ export class Group extends TimeStamps implements Base {
throw new Error('Not Found Group'); throw new Error('Not Found Group');
} }
if (String(group.owner) === userId) {
// 群组管理者有所有权限
return [...allPermission];
}
const member = group.members.find( const member = group.members.find(
(member) => String(member.userId) === userId (member) => String(member.userId) === userId
); );
@ -271,7 +266,24 @@ export class Group extends TimeStamps implements Base {
return p?.permissions ?? []; 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,
]);
}
} }
/** /**

@ -13,6 +13,7 @@ export type {
export { parseLanguageFromHead } from './services/lib/i18n/parser'; export { parseLanguageFromHead } from './services/lib/i18n/parser';
export { t } from './services/lib/i18n'; export { t } from './services/lib/i18n';
export * from './services/lib/errors'; export * from './services/lib/errors';
export { PERMISSION, allPermission } from './services/lib/role';
export { call } from './services/lib/call'; export { call } from './services/lib/call';
export { export {
config, config,

@ -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) { export function call(ctx: TcContext) {
return { 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)
);
}, },
}; };
} }

@ -3,10 +3,14 @@ import {
TcPureContext, TcPureContext,
TcContext, TcContext,
TcDbService, TcDbService,
call,
NoPermissionError,
} from 'tailchat-server-sdk'; } from 'tailchat-server-sdk';
import type { WebhookEvent } from '@octokit/webhooks-types'; import type { WebhookEvent } from '@octokit/webhooks-types';
import type { SubscribeDocument, SubscribeModel } from '../models/subscribe'; import type { SubscribeDocument, SubscribeModel } from '../models/subscribe';
const PERMISSION_MANAGE = 'plugin.com.msgbyte.github.subscribe.manage';
/** /**
* Github * Github
*/ */
@ -79,16 +83,19 @@ class GithubSubscribeService extends TcService {
}> }>
) { ) {
const { groupId, textPanelId, repoName } = ctx.params; const { groupId, textPanelId, repoName } = ctx.params;
const { userId, t } = ctx.meta;
if (!groupId || !textPanelId || !repoName) { if (!groupId || !textPanelId || !repoName) {
throw new Error('参数不全'); throw new Error('参数不全');
} }
const isGroupOwner = await ctx.call('group.isGroupOwner', { const [hasPermission] = await call(ctx).checkUserPermissions(
groupId, groupId,
}); userId,
if (isGroupOwner !== true) { [PERMISSION_MANAGE]
throw new Error('没有操作权限'); );
if (!hasPermission) {
throw new NoPermissionError(t('没有操作权限'));
} }
// TODO: 需要检查textPanelId是否合法 // TODO: 需要检查textPanelId是否合法
@ -109,6 +116,16 @@ class GithubSubscribeService extends TcService {
}> }>
) { ) {
const groupId = ctx.params.groupId; 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 const docs = await this.adapter.model
.find({ .find({
@ -129,11 +146,15 @@ class GithubSubscribeService extends TcService {
}> }>
) { ) {
const { groupId, subscribeId } = ctx.params; const { groupId, subscribeId } = ctx.params;
const isGroupOwner = await ctx.call('group.isGroupOwner', { const { userId, t } = ctx.meta;
const [hasPermission] = await call(ctx).checkUserPermissions(
groupId, groupId,
}); userId,
if (isGroupOwner !== true) { [PERMISSION_MANAGE]
throw new Error('没有操作权限'); );
if (!hasPermission) {
throw new NoPermissionError(t('没有删除权限'));
} }
await this.adapter.model.deleteOne({ await this.adapter.model.deleteOne({

@ -1,4 +1,9 @@
import { regCustomPanel, Loadable, regInspectService } from '@capital/common'; import {
regCustomPanel,
Loadable,
regInspectService,
regPluginPermission,
} from '@capital/common';
import { Translate } from './translate'; import { Translate } from './translate';
regCustomPanel({ regCustomPanel({
@ -12,3 +17,10 @@ regInspectService({
name: 'plugin:com.msgbyte.github.subscribe', name: 'plugin:com.msgbyte.github.subscribe',
label: Translate.githubService, label: Translate.githubService,
}); });
regPluginPermission({
key: 'plugin.com.msgbyte.github.subscribe.manage',
title: 'Github 订阅管理',
desc: '允许管理Github订阅列表',
default: false,
});

@ -1,2 +1,299 @@
declare module '@capital/common'; /* eslint-disable @typescript-eslint/no-explicit-any */
declare module '@capital/component';
/**
* 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<any>;
post: (actionName: string, data?: any, config?: any) => Promise<any>;
};
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;
}>;
}

@ -18,9 +18,9 @@ import {
DataNotFoundError, DataNotFoundError,
EntityError, EntityError,
NoPermissionError, NoPermissionError,
PERMISSION,
} from 'tailchat-server-sdk'; } from 'tailchat-server-sdk';
import moment from 'moment'; import moment from 'moment';
import { PERMISSION } from '../../../lib/role';
interface GroupService interface GroupService
extends TcService, extends TcService,

@ -11,8 +11,8 @@ import {
PureContext, PureContext,
call, call,
NoPermissionError, NoPermissionError,
PERMISSION,
} from 'tailchat-server-sdk'; } from 'tailchat-server-sdk';
import { PERMISSION } from '../../../lib/role';
interface GroupService interface GroupService
extends TcService, extends TcService,

Loading…
Cancel
Save