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');
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('发送消息'),

@ -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",

@ -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<RolePermissionProps> = React.memo(
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>
);
}

@ -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]
);

@ -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<PermissionItemType>();

@ -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;
}
/**

@ -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"

@ -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

@ -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,
]);
}
}
/**

@ -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,

@ -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)
);
},
};
}

@ -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({

@ -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,
});

@ -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<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,
EntityError,
NoPermissionError,
PERMISSION,
} from 'tailchat-server-sdk';
import moment from 'moment';
import { PERMISSION } from '../../../lib/role';
interface GroupService
extends TcService,

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

Loading…
Cancel
Save