diff --git a/config/config.ts b/config/config.ts index a65bba68d37de876a41d1db8e1a450efd715b145..5a13406462c259d7a0788272f8b93cb3a488a04d 100644 --- a/config/config.ts +++ b/config/config.ts @@ -54,15 +54,6 @@ const plugins: IPlugin[] = [ autoAddMenu: true, }, ], - // ...(!process.env.TEST && os.platform() === 'darwin' - // ? { - // dll: { - // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'], - // exclude: ['@babel/runtime'], - // }, - // hardSource: true, - // } - // : {}), ]; // 针对 preview.pro.ant.design 的 GA 统计代码 diff --git a/package.json b/package.json index c7a6f6a494da9611fbe53b7389dd281a8675f039..42626d912893f186223f00d24905fa70b006d6b6 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "not ie <= 10" ], "dependencies": { - "@ant-design/pro-layout": "^4.0.3", + "@ant-design/pro-layout": "^4.1.0", "@antv/data-set": "^0.10.1", "antd": "^3.16.1", "bizcharts": "^3.4.3", diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a8bbb5e049dd919b611b27eb42ca692f09fb049b --- /dev/null +++ b/src/components/Authorized/Authorized.tsx @@ -0,0 +1,29 @@ +import CheckPermissions from './CheckPermissions'; +import { IAuthorityType } from './CheckPermissions'; +import Secured from './Secured'; +import check from './CheckPermissions'; +import AuthorizedRoute from './AuthorizedRoute'; +import React from 'react'; + +interface IAuthorizedProps { + authority: IAuthorityType; + noMatch?: React.ReactNode; +} + +type IAuthorizedType = React.FunctionComponent & { + Secured: typeof Secured; + check: typeof check; + AuthorizedRoute: typeof AuthorizedRoute; +}; + +const Authorized: React.FunctionComponent = ({ + children, + authority, + noMatch = null, +}) => { + const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; + const dom = CheckPermissions(authority, childrenRender, noMatch); + return <>{dom}; +}; + +export default Authorized as IAuthorizedType; diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ceb0b25a71522f324fc50a0115c95f0e492caddd --- /dev/null +++ b/src/components/Authorized/AuthorizedRoute.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Route, Redirect } from 'umi'; +import Authorized from './Authorized'; +import { IAuthorityType } from './CheckPermissions'; + +interface IAuthorizedRoutePops { + currentAuthority: string; + component: React.ComponentClass; + render: () => React.ReactNode; + redirectPath: string; + authority: IAuthorityType; +} + +const AuthorizedRoute: React.SFC = ({ + component: Component, + render, + authority, + redirectPath, + ...rest +}) => ( + } />} + > + (Component ? : render(props))} + /> + +); + +export default AuthorizedRoute; diff --git a/src/components/Authorized/CheckPermissions.test.js b/src/components/Authorized/CheckPermissions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3988d85a18fcfe7412bb72f672ffdf7c2501d8c2 --- /dev/null +++ b/src/components/Authorized/CheckPermissions.test.js @@ -0,0 +1,55 @@ +import { checkPermissions } from './CheckPermissions'; + +const target = 'ok'; +const error = 'error'; + +describe('test CheckPermissions', () => { + it('Correct string permission authentication', () => { + expect(checkPermissions('user', 'user', target, error)).toEqual('ok'); + }); + it('Correct string permission authentication', () => { + expect(checkPermissions('user', 'NULL', target, error)).toEqual('error'); + }); + it('authority is undefined , return ok', () => { + expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok'); + }); + it('currentAuthority is undefined , return error', () => { + expect(checkPermissions('admin', null, target, error)).toEqual('error'); + }); + it('Wrong string permission authentication', () => { + expect(checkPermissions('admin', 'user', target, error)).toEqual('error'); + }); + it('Correct Array permission authentication', () => { + expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual('ok'); + }); + it('Wrong Array permission authentication,currentAuthority error', () => { + expect(checkPermissions(['user', 'admin'], 'user,admin', target, error)).toEqual('error'); + }); + it('Wrong Array permission authentication', () => { + expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual('error'); + }); + it('Wrong Function permission authentication', () => { + expect(checkPermissions(() => false, 'guest', target, error)).toEqual('error'); + }); + it('Correct Function permission authentication', () => { + expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok'); + }); + it('authority is string, currentAuthority is array, return ok', () => { + expect(checkPermissions('user', ['user'], target, error)).toEqual('ok'); + }); + it('authority is string, currentAuthority is array, return ok', () => { + expect(checkPermissions('user', ['user', 'admin'], target, error)).toEqual('ok'); + }); + it('authority is array, currentAuthority is array, return ok', () => { + expect(checkPermissions(['user', 'admin'], ['user', 'admin'], target, error)).toEqual('ok'); + }); + it('Wrong Function permission authentication', () => { + expect(checkPermissions(() => false, ['user'], target, error)).toEqual('error'); + }); + it('Correct Function permission authentication', () => { + expect(checkPermissions(() => true, ['user'], target, error)).toEqual('ok'); + }); + it('authority is undefined , return ok', () => { + expect(checkPermissions(null, ['user'], target, error)).toEqual('ok'); + }); +}); diff --git a/src/components/Authorized/CheckPermissions.tsx b/src/components/Authorized/CheckPermissions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4b18a41c41cc60b476e6fb7601e6e906286ff22 --- /dev/null +++ b/src/components/Authorized/CheckPermissions.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +// eslint-disable-next-line import/no-cycle +import PromiseRender from './PromiseRender'; +import { CURRENT } from './renderAuthorize'; + +export type IAuthorityType = + | string + | string[] + | Promise + | ((currentAuthority: string | string[]) => IAuthorityType); + +/** + * 通用权限检查方法 + * Common check permissions method + * @param { 权限判定 | Permission judgment } authority + * @param { 你的权限 | Your permission description } currentAuthority + * @param { 通过的组件 | Passing components } target + * @param { 未通过的组件 | no pass components } Exception + */ +const checkPermissions = ( + authority: IAuthorityType, + currentAuthority: string | string[], + target: React.ComponentClass | React.ReactNode, + Exception: React.ReactNode, +): React.ReactNode => { + // 没有判定权限.默认查看所有 + // Retirement authority, return target; + if (!authority) { + return target; + } + // 数组处理 + if (Array.isArray(authority)) { + if (Array.isArray(currentAuthority)) { + if (currentAuthority.some(item => authority.includes(item))) { + return target; + } + } else if (authority.includes(currentAuthority)) { + return target; + } + return Exception; + } + // string 处理 + if (typeof authority === 'string') { + if (Array.isArray(currentAuthority)) { + if (currentAuthority.some(item => authority === item)) { + return target; + } + } else if (authority === currentAuthority) { + return target; + } + return Exception; + } + // Promise 处理 + if (authority instanceof Promise) { + return ; + } + // Function 处理 + if (typeof authority === 'function') { + try { + const bool = authority(currentAuthority); + // 函数执行后返回值是 Promise + if (bool instanceof Promise) { + return ; + } + if (bool) { + return target; + } + return Exception; + } catch (error) { + throw error; + } + } + throw new Error('unsupported parameters'); +}; + +export { checkPermissions }; + +const check = ( + authority: IAuthorityType, + target: React.ComponentClass | React.ReactNode, + Exception: React.ReactNode, +): React.ReactNode => checkPermissions(authority, CURRENT, target, Exception); + +export default check; diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx new file mode 100644 index 0000000000000000000000000000000000000000..12f18ed22e63a7c11ff842099e4cfcc10f86a361 --- /dev/null +++ b/src/components/Authorized/PromiseRender.tsx @@ -0,0 +1,91 @@ +import { Spin } from 'antd'; +import isEqual from 'lodash/isEqual'; +import React from 'react'; +// eslint-disable-next-line import/no-cycle +import { isComponentClass } from './Secured'; + +interface IPromiseRenderProps { + ok: React.ReactNode; + error: React.ReactNode; + promise: Promise; +} + +interface IPromiseRenderState { + component: React.ComponentClass | React.FunctionComponent; +} + +export default class PromiseRender extends React.Component< + IPromiseRenderProps, + IPromiseRenderState +> { + state: IPromiseRenderState = { + component: () => null, + }; + + componentDidMount() { + this.setRenderComponent(this.props); + } + + shouldComponentUpdate = (nextProps: IPromiseRenderProps, nextState: IPromiseRenderState) => { + const { component } = this.state; + if (!isEqual(nextProps, this.props)) { + this.setRenderComponent(nextProps); + } + if (nextState.component !== component) return true; + return false; + }; + + // set render Component : ok or error + setRenderComponent(props: IPromiseRenderProps) { + const ok = this.checkIsInstantiation(props.ok); + const error = this.checkIsInstantiation(props.error); + props.promise + .then(() => { + this.setState({ + component: ok, + }); + }) + .catch(() => { + this.setState({ + component: error, + }); + }); + } + + // Determine whether the incoming component has been instantiated + // AuthorizedRoute is already instantiated + // Authorized render is already instantiated, children is no instantiated + // Secured is not instantiated + checkIsInstantiation = ( + target: React.ReactNode | React.ComponentClass, + ): React.FunctionComponent => { + if (isComponentClass(target)) { + const Target = target as React.ComponentClass; + return (props: any) => ; + } + if (React.isValidElement(target)) { + return (props: any) => React.cloneElement(target, props); + } + return () => target as (React.ReactNode & null); + }; + + render() { + const { component: Component } = this.state; + const { ok, error, promise, ...rest } = this.props; + return Component ? ( + + ) : ( +
+ +
+ ); + } +} diff --git a/src/components/Authorized/Secured.tsx b/src/components/Authorized/Secured.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4582ed091d8497c85e99fefcf53d1bc3e094a4d4 --- /dev/null +++ b/src/components/Authorized/Secured.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import CheckPermissions from './CheckPermissions'; + +/** + * 默认不能访问任何页面 + * default is "NULL" + */ +const Exception403 = () => 403; + +export const isComponentClass = ( + component: React.ComponentClass | React.ReactNode, +): boolean => { + if (!component) return false; + const proto = Object.getPrototypeOf(component); + if (proto === React.Component || proto === Function.prototype) return true; + return isComponentClass(proto); +}; + +// Determine whether the incoming component has been instantiated +// AuthorizedRoute is already instantiated +// Authorized render is already instantiated, children is no instantiated +// Secured is not instantiated +const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => { + if (isComponentClass(target)) { + const Target = target as React.ComponentClass; + return (props: any) => ; + } + if (React.isValidElement(target)) { + return (props: any) => React.cloneElement(target, props); + } + return () => target; +}; + +/** + * 用于判断是否拥有权限访问此 view 权限 + * authority 支持传入 string, () => boolean | Promise + * e.g. 'user' 只有 user 用户能访问 + * e.g. 'user,admin' user 和 admin 都能访问 + * e.g. ()=>boolean 返回true能访问,返回false不能访问 + * e.g. Promise then 能访问 catch不能访问 + * e.g. authority support incoming string, () => boolean | Promise + * e.g. 'user' only user user can access + * e.g. 'user, admin' user and admin can access + * e.g. () => boolean true to be able to visit, return false can not be accessed + * e.g. Promise then can not access the visit to catch + * @param {string | function | Promise} authority + * @param {ReactNode} error 非必需参数 + */ +const authorize = (authority: string, error?: React.ReactNode) => { + /** + * conversion into a class + * 防止传入字符串时找不到staticContext造成报错 + * String parameters can cause staticContext not found error + */ + let classError: boolean | React.FunctionComponent = false; + if (error) { + classError = (() => error) as React.FunctionComponent; + } + if (!authority) { + throw new Error('authority is required'); + } + return function decideAuthority(target: React.ComponentClass | React.ReactNode) { + const component = CheckPermissions(authority, target, classError || Exception403); + return checkIsInstantiation(component); + }; +}; + +export default authorize; diff --git a/src/components/Authorized/index.tsx b/src/components/Authorized/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..467aade677a0f92fe84b7553eb2ea0bce8a72fd4 --- /dev/null +++ b/src/components/Authorized/index.tsx @@ -0,0 +1,12 @@ +import Authorized from './Authorized'; +import AuthorizedRoute from './AuthorizedRoute'; +import Secured from './Secured'; +import check from './CheckPermissions'; +import renderAuthorize from './renderAuthorize'; + +Authorized.Secured = Secured; +Authorized.AuthorizedRoute = AuthorizedRoute; +Authorized.check = check; + +const RenderAuthorize = renderAuthorize(Authorized); +export default RenderAuthorize; diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts new file mode 100644 index 0000000000000000000000000000000000000000..690c9becedc59e222a1db0ad270f7b5e3bb2e3b3 --- /dev/null +++ b/src/components/Authorized/renderAuthorize.ts @@ -0,0 +1,27 @@ +/* eslint-disable import/no-mutable-exports */ +let CURRENT: string | string[] = 'NULL'; +/** + * use authority or getAuthority + * @param {string|()=>String} currentAuthority + */ +const renderAuthorize = (Authorized: any) => ( + currentAuthority: string | string[] | (() => typeof CURRENT), +) => { + if (currentAuthority) { + if (typeof currentAuthority === 'function') { + CURRENT = currentAuthority(); + } + if ( + Object.prototype.toString.call(currentAuthority) === '[object String]' || + Array.isArray(currentAuthority) + ) { + CURRENT = currentAuthority as string[]; + } + } else { + CURRENT = 'NULL'; + } + return Authorized; +}; + +export { CURRENT }; +export default (Authorized: any) => renderAuthorize(Authorized); diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index f6060c8134f8ec37c1d883d818e14ddd6d1e547b..5cde19e2e4203f56705fd16130d9edd0af576fd4 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -10,6 +10,7 @@ import { BasicLayoutProps as BasicLayoutComponentsProps, MenuDataItem, Settings, + SettingDrawer, } from '@ant-design/pro-layout'; export interface BasicLayoutProps extends BasicLayoutComponentsProps, ConnectProps { @@ -39,21 +40,25 @@ const BasicLayout: React.FC = props => { dispatch!({ type: 'global/changeLayoutCollapsed', payload }); return ( - - dispatch!({ - type: 'settings/changeSetting', - payload: settings, - }) - } - onChangeLayoutCollapsed={handleMenuCollapse} - renderRightContent={RightProps => } - {...props} - > - {children} - + <> + } + {...props} + > + {children} + + + dispatch!({ + type: 'settings/changeSetting', + payload: settings, + }) + } + /> + ); }; diff --git a/src/utils/Authorized.ts b/src/utils/Authorized.ts index 4143571a83d00cd0b6a1497e46692cf90c0bbe62..b2a97e5087325beb7678d3f453936562377382e5 100644 --- a/src/utils/Authorized.ts +++ b/src/utils/Authorized.ts @@ -1,4 +1,4 @@ -import { Authorized as RenderAuthorized } from 'ant-design-pro'; +import { default as RenderAuthorized } from '@/components/Authorized'; import { getAuthority } from './authority'; let Authorized = RenderAuthorized(getAuthority()); // eslint-disable-line