Unverified Commit cfa27b18 authored by ddcat1115's avatar ddcat1115 Committed by GitHub

Authority management (#508)

* temp save

* rebase master

* fix same error

* Add a new user and different permissions

* fix eol-last

* add Secured decorator

* fix list redirect bug (#507)

* Mobile menu (#463)

* Increase the sliding menu

* Add a simple animation

* update mobile menu

* update

* update

* update

* rebase master

* recovery import/first

* fix error

* Fix some bugs
Change "ALL" to "NONE"
Remove the "!" Support
After landing successfully reload
Reset the format

* Pump your public logic

* add some test

* Add documents

* use default currentRole in Authorized/AuthorizedRoute

* rename props & change some authority setting

* A big change
😄 unified router and Secured parameters
😭 loginOut logout also changed to reload

* fix siderMeun bugs

* Decoupled SiderMenu

* Remove the handsome head of information

* Add a simple error

* rebase master
parent 74f0a0aa
......@@ -15,3 +15,4 @@ npm-debug.log*
/coverage
.idea
yarn.lock
......@@ -70,13 +70,30 @@ const proxy = {
'GET /api/profile/advanced': getProfileAdvancedData,
'POST /api/login/account': (req, res) => {
const { password, userName, type } = req.body;
if(password === '888888' && userName === 'admin'){
res.send({
status: 'ok',
type,
currentAuthority: 'admin'
});
return ;
}
if(password === '123456' && userName === 'user'){
res.send({
status: 'ok',
type,
currentAuthority: 'user'
});
return ;
}
res.send({
status: password === '888888' && userName === 'admin' ? 'ok' : 'error',
status: 'error',
type,
currentAuthority: 'guest'
});
},
'POST /api/register': (req, res) => {
res.send({ status: 'ok' });
res.send({ status: 'ok', currentAuthority: 'user' });
},
'GET /api/notices': getNotices,
'GET /api/500': (req, res) => {
......
......@@ -16,11 +16,12 @@
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js",
"test": "jest",
"test:comps": "jest ./src/components",
"test:all": "node ./tests/run-tests.js"
},
"dependencies": {
"@antv/data-set": "^0.8.0",
"antd": "^3.0.0",
"antd": "^3.1.0",
"babel-polyfill": "^6.26.0",
"babel-runtime": "^6.9.2",
"bizcharts": "^3.1.0-beta.4",
......@@ -37,10 +38,10 @@
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"rc-drawer-menu": "^0.5.0",
"react": "^16.0.0",
"react": "^16.2.0",
"react-container-query": "^0.9.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.0.0",
"react-dom": "^16.2.0",
"react-fittext": "^1.0.0"
},
"devDependencies": {
......
......@@ -25,6 +25,7 @@ const menuData = [{
path: 'step-form',
}, {
name: '高级表单',
authority: 'admin',
path: 'advanced-form',
}],
}, {
......@@ -64,6 +65,7 @@ const menuData = [{
}, {
name: '高级详情页',
path: 'advanced',
authority: 'admin',
}],
}, {
name: '结果页',
......@@ -83,6 +85,7 @@ const menuData = [{
children: [{
name: '403',
path: '403',
authority: 'user',
}, {
name: '404',
path: '404',
......@@ -97,6 +100,7 @@ const menuData = [{
name: '账户',
icon: 'user',
path: 'user',
authority: 'guest',
children: [{
name: '登录',
path: 'login',
......@@ -114,14 +118,15 @@ const menuData = [{
target: '_blank',
}];
function formatter(data, parentPath = '') {
function formatter(data, parentPath = '', parentAuthority) {
return data.map((item) => {
const result = {
...item,
path: `${parentPath}${item.path}`,
authority: item.authority || parentAuthority,
};
if (item.children) {
result.children = formatter(item.children, `${parentPath}${item.path}/`);
result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority);
}
return result;
});
......
......@@ -21,17 +21,17 @@ function getFlatMenuData(menus) {
let keys = {};
menus.forEach((item) => {
if (item.children) {
keys[item.path] = item.name;
keys[item.path] = { ...item };
keys = { ...keys, ...getFlatMenuData(item.children) };
} else {
keys[item.path] = item.name;
keys[item.path] = { ...item };
}
});
return keys;
}
export const getRouterData = (app) => {
const routerData = {
const routerConfig = {
'/': {
component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')),
},
......@@ -45,6 +45,7 @@ export const getRouterData = (app) => {
component: dynamicWrapper(app, ['project', 'activities', 'chart'], () => import('../routes/Dashboard/Workplace')),
// hideInBreadcrumb: true,
// name: '工作台',
// authority: 'admin',
},
'/form/basic-form': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/BasicForm')),
......@@ -127,12 +128,14 @@ export const getRouterData = (app) => {
};
// Get name from ./menu.js or just set it in the router data.
const menuData = getFlatMenuData(getMenuData());
const routerDataWithName = {};
Object.keys(routerData).forEach((item) => {
routerDataWithName[item] = {
...routerData[item],
name: routerData[item].name || menuData[item.replace(/^\//, '')],
const routerData = {};
Object.keys(routerConfig).forEach((item) => {
const menuItem = menuData[item.replace(/^\//, '')] || {};
routerData[item] = {
...routerConfig[item],
name: routerConfig[item].name || menuItem.name,
authority: routerConfig[item].authority || menuItem.authority,
};
});
return routerDataWithName;
return routerData;
};
import React from 'react';
import CheckPermissions from './CheckPermissions';
class Authorized extends React.Component {
render() {
const { children, authority, noMatch = null } = this.props;
const childrenRender = typeof children === 'undefined' ? null : children;
return CheckPermissions(
authority,
childrenRender,
noMatch
);
}
}
export default Authorized;
import React from 'react';
import { Route, Redirect } from 'dva/router';
import Authorized from './Authorized';
class AuthorizedRoute extends React.Component {
render() {
const { component: Component, render, authority,
redirectPath, ...rest } = this.props;
return (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={props => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
);
}
}
export default AuthorizedRoute;
import React from 'react';
import PromiseRender from './PromiseRender';
import { CURRENT } from './index';
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 Permission judgment type string |array | Promise | Function } authority
* @param { 你的权限 Your permission description type:string} currentAuthority
* @param { 通过的组件 Passing components } target
* @param { 未通过的组件 no pass components } Exception
*/
const checkPermissions = (authority, currentAuthority, target, Exception) => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (authority.constructor.name === 'Array') {
if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 处理
if (authority.constructor.name === 'String') {
if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Promise 处理
if (authority.constructor.name === 'Promise') {
return () => (
<PromiseRender ok={target} error={Exception} promise={authority} />
);
}
// Function 处理
if (authority.constructor.name === 'Function') {
try {
const bool = authority();
if (bool) {
return target;
}
return Exception;
} catch (error) {
throw error;
}
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
const check = (authority, target, Exception) => {
return checkPermissions(authority, CURRENT, target, Exception);
};
export default check;
import { checkPermissions } from './CheckPermissions.js';
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('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');
});
});
import React from 'react';
import { Spin } from 'antd';
export default class PromiseRender extends React.PureComponent {
state = {
component: false,
};
async componentDidMount() {
this.props.promise
.then(() => {
this.setState({
component: this.props.ok,
});
})
.catch(() => {
this.setState({
component: this.props.error,
});
});
}
render() {
const C = this.state.component;
return C ? (
<C {...this.props} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}
import React from 'react';
import Exception from '../Exception/index';
import CheckPermissions from './CheckPermissions';
/**
* 默认不能访问任何页面
* default is "NULL"
*/
const Exception403 = () => (
<Exception type="403" style={{ minHeight: 500, height: '80%' }} />
);
/**
* 用于判断是否拥有权限访问此view权限
* authority 支持传入 string ,funtion:()=>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, funtion: () => 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, error) => {
/**
* conversion into a class
* 防止传入字符串时找不到staticContext造成报错
* String parameters can cause staticContext not found error
*/
let classError = false;
if (error) {
classError = () => error;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(targer) {
return CheckPermissions(
authority,
targer,
classError || Exception403
);
};
};
export default authorize;
import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions.js';
/* eslint-disable import/no-mutable-exports */
let CURRENT = 'NULL';
Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = (currentAuthority) => {
if (currentAuthority) {
if (currentAuthority.constructor.name === 'Function') {
CURRENT = currentAuthority();
}
if (currentAuthority.constructor.name === 'String') {
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default renderAuthorize;
......@@ -77,62 +77,98 @@ export default class SiderMenu extends PureComponent {
return itemRegExp.test(path.replace(/^\//, '').replace(/\/$/, ''));
});
}
getNavMenuItems(menusData) {
/**
* 判断是否是http链接.返回 Link 或 a
* Judge whether it is http link.return a or Link
* @memberof SiderMenu
*/
getMenuItemPath = (item) => {
const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon);
const { target, name } = item;
// Is it a http link
if (/^https?:\/\//.test(itemPath)) {
return (
<a href={itemPath} target={target}>
{icon}<span>{name}</span>
</a>
);
}
return (
<Link
to={itemPath}
target={target}
replace={itemPath === this.props.location.pathname}
onClick={this.props.isMobile ? () => { this.props.onCollapse(true); } : undefined}
>
{icon}<span>{name}</span>
</Link>
);
}
/**
* get SubMenu or Item
*/
getSubMenuOrItem=(item) => {
if (item.children && item.children.some(child => child.name)) {
return (
<SubMenu
title={
item.icon ? (
<span>
{getIcon(item.icon)}
<span>{item.name}</span>
</span>
) : item.name
}
key={item.key || item.path}
>
{this.getNavMenuItems(item.children)}
</SubMenu>
);
} else {
return (
<Menu.Item key={item.key || item.path}>
{this.getMenuItemPath(item)}
</Menu.Item>
);
}
}
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
getNavMenuItems = (menusData) => {
if (!menusData) {
return [];
}
return menusData.map((item) => {
if (!item.name) {
if (!item.name || item.hideInMenu) {
return null;
}
let itemPath;
if (item.path && item.path.indexOf('http') === 0) {
itemPath = item.path;
} else {
itemPath = `/${item.path || ''}`.replace(/\/+/g, '/');
}
if (item.children && item.children.some(child => child.name)) {
return item.hideInMenu ? null :
(
<SubMenu
title={
item.icon ? (
<span>
{getIcon(item.icon)}
<span>{item.name}</span>
</span>
) : item.name
}
key={item.key || item.path}
>
{this.getNavMenuItems(item.children)}
</SubMenu>
);
}
const icon = getIcon(item.icon);
return item.hideInMenu ? null :
(
<Menu.Item key={item.key || item.path}>
{
/^https?:\/\//.test(itemPath) ? (
<a href={itemPath} target={item.target}>
{icon}<span>{item.name}</span>
</a>
) : (
<Link
to={itemPath}
target={item.target}
replace={itemPath === this.props.location.pathname}
onClick={this.props.isMobile ? () => { this.props.onCollapse(true); } : undefined}
>
{icon}<span>{item.name}</span>
</Link>
)
}
</Menu.Item>
);
const ItemDom = this.getSubMenuOrItem(item);
return this.checkPermissionItem(item.authority, ItemDom);
});
}
// conversion Path
// 转化路径
conversionPath=(path) => {
if (path && path.indexOf('http') === 0) {
return path;
} else {
return `/${path || ''}`.replace(/\/+/g, '/');
}
}
// permission to check
checkPermissionItem = (authority, ItemDom) => {
if (this.props.Authorized && this.props.Authorized.check) {
const { check } = this.props.Authorized;
return check(
authority,
ItemDom
);
}
return ItemDom;
}
handleOpenChange = (openKeys) => {
const lastOpenKey = openKeys[openKeys.length - 1];
const isMainMenu = this.menus.some(
......
......@@ -12,9 +12,13 @@ import GlobalFooter from '../components/GlobalFooter';
import SiderMenu from '../components/SiderMenu';
import NotFound from '../routes/Exception/404';
import { getRoutes } from '../utils/utils';
import Authorized from '../utils/Authorized';
import { getMenuData } from '../common/menu';
import logo from '../assets/logo.svg';
const { Content } = Layout;
const { AuthorizedRoute } = Authorized;
/**
* 根据菜单取得重定向地址.
*/
......@@ -34,7 +38,6 @@ const getRedirect = (item) => {
};
getMenuData().forEach(getRedirect);
const { Content } = Layout;
const query = {
'screen-xs': {
maxWidth: 575,
......@@ -130,6 +133,10 @@ class BasicLayout extends React.PureComponent {
<Layout>
<SiderMenu
logo={logo}
// 不带Authorized参数的情况下如果没有权限,会强制跳到403界面
// If you do not have the Authorized parameter
// you will be forced to jump to the 403 interface without permission
Authorized={Authorized}
menuData={getMenuData()}
collapsed={collapsed}
location={location}
......@@ -153,19 +160,23 @@ class BasicLayout extends React.PureComponent {
<div style={{ minHeight: 'calc(100vh - 260px)' }}>
<Switch>
{
redirectData.map(item =>
<Redirect key={item.from} exact from={item.from} to={item.to} />
getRoutes(match.path, routerData).map(item =>
(
<AuthorizedRoute
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
authority={item.authority}
redirectPath="/exception/403"
/>
)
)
}
{
getRoutes(match.path, routerData).map(item => (
<Route
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
/>
))
redirectData.map(item =>
<Redirect key={item.from} exact from={item.from} to={item.to} />
)
}
<Redirect exact from="/" to="/dashboard/analysis" />
<Route render={NotFound} />
......
import { routerRedux } from 'dva/router';
import { fakeAccountLogin } from '../services/api';
import { setAuthority } from '../utils/authority';
export default {
namespace: 'login',
......@@ -21,7 +21,11 @@ export default {
});
// Login successfully
if (response.status === 'ok') {
yield put(routerRedux.push('/'));
// 非常粗暴的跳转,登陆成功之后权限会变成user或admin,会自动重定向到主页
// Login success after permission changes to admin or user
// The refresh will automatically redirect to the home page
// yield put(routerRedux.push('/'));
location.reload();
}
},
*logout(_, { put }) {
......@@ -29,14 +33,19 @@ export default {
type: 'changeLoginStatus',
payload: {
status: false,
currentAuthority: 'guest',
},
});
yield put(routerRedux.push('/user/login'));
// yield put(routerRedux.push('/user/login'));
// Login out after permission changes to admin or user
// The refresh will automatically redirect to the login page
location.reload();
},
},
reducers: {
changeLoginStatus(state, { payload }) {
setAuthority(payload.currentAuthority);
return {
...state,
status: payload.status,
......
import React from 'react';
import { Router, Route, Switch } from 'dva/router';
import { Router, Switch } from 'dva/router';
import { LocaleProvider, Spin } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import dynamic from 'dva/dynamic';
import { getRouterData } from './common/router';
import Authorized from './utils/Authorized';
import styles from './index.less';
const { AuthorizedRoute } = Authorized;
dynamic.setDefaultLoadingComponent(() => {
return <Spin size="large" className={styles.globalSpin} />;
});
......@@ -19,8 +20,18 @@ function RouterConfig({ history, app }) {
<LocaleProvider locale={zhCN}>
<Router history={history}>
<Switch>
<Route path="/user" render={props => <UserLayout {...props} />} />
<Route path="/" render={props => <BasicLayout {...props} />} />
<AuthorizedRoute
path="/user"
render={props => <UserLayout {...props} />}
authority="guest"
redirectPath="/"
/>
<AuthorizedRoute
path="/"
render={props => <BasicLayout {...props} />}
authority={['admin', 'user']}
redirectPath="/user/login"
/>
</Switch>
</Router>
</LocaleProvider>
......
......@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Tooltip } from 'antd';
import numeral from 'numeral';
import { Secured } from '../../utils/Authorized';
import { Pie, WaterWave, Gauge, TagCloud } from '../../components/Charts';
import NumberInfo from '../../components/NumberInfo';
import CountDown from '../../components/CountDown';
......@@ -12,6 +12,7 @@ import styles from './Monitor.less';
const targetTime = new Date().getTime() + 3900000;
@Secured('admin')
@connect(state => ({
monitor: state.monitor,
}))
......@@ -40,7 +41,10 @@ export default class Monitor extends PureComponent {
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo subTitle="销售目标完成率" total="92%" />
<NumberInfo
subTitle="销售目标完成率"
total="92%"
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo subTitle="活动剩余时间" total={<CountDown target={targetTime} />} />
......@@ -55,10 +59,7 @@ export default class Monitor extends PureComponent {
</Row>
<div className={styles.mapChart}>
<Tooltip title="等待后期实现">
<img
src="https://gw.alipayobjects.com/zos/rmsportal/HBWnDEUXCnGnGrRfrpKa.png"
alt="map"
/>
<img src="https://gw.alipayobjects.com/zos/rmsportal/HBWnDEUXCnGnGrRfrpKa.png" alt="map" />
</Tooltip>
</div>
</Card>
......@@ -140,17 +141,20 @@ export default class Monitor extends PureComponent {
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card title="热门搜索" bordered={false}>
<TagCloud data={tags} height={161} />
<Card title="热门搜索" bordered={false} bodyStyle={{ overflow: 'hidden' }}>
<TagCloud
data={tags}
height={161}
/>
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card
title="资源剩余"
bodyStyle={{ textAlign: 'center', fontSize: 0 }}
bordered={false}
>
<WaterWave height={161} title="补贴资金剩余" percent={34} />
<Card title="资源剩余" bodyStyle={{ textAlign: 'center', fontSize: 0 }} bordered={false}>
<WaterWave
height={161}
title="补贴资金剩余"
percent={34}
/>
</Card>
</Col>
</Row>
......
......@@ -62,8 +62,8 @@ export default class LoginPage extends Component {
login.submitting === false &&
this.renderMessage('账户或密码错误')
}
<UserName name="userName" />
<Password name="password" />
<UserName name="userName" placeholder="admin/user" />
<Password name="password" placeholder="888888/123456" />
</Tab>
<Tab key="mobile" tab="手机号登录">
{
......
import RenderAuthorized from '../components/Authorized';
import { getAuthority } from './authority';
const Authorized = RenderAuthorized(getAuthority());
export default Authorized;
// use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority() {
return localStorage.getItem('antd-pro-authority') || 'guest';
}
export function setAuthority(authority) {
return localStorage.setItem('antd-pro-authority', authority);
}
......@@ -124,9 +124,9 @@ export function getRoutes(path, routerData) {
const renderRoutes = renderArr.map((item) => {
const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
return {
...routerData[`${path}${item}`],
key: `${path}${item}`,
path: `${path}${item}`,
component: routerData[`${path}${item}`].component,
exact,
};
});
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment