diff --git a/README.md b/README.md index 2086926..11e7bcd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

- Downloads + Downloads Build Status Build Status - Coverage Status - Downloads + Coverage Status + Downloads Downloads @@ -22,7 +22,7 @@ * 极简封装了多租户底层,用更少的代码换来拓展性更强的SaaS多租户系统。 * 借鉴OAuth2,实现了多终端认证系统,可控制子系统的token权限互相隔离。 * 借鉴Security,封装了Secure模块,采用JWT做Token认证,可拓展集成Redis等细颗粒度控制方案。 -* 稳定生产了一年,经历了从Camden -> Greenwich的技术架构,也经历了从fat jar -> docker -> k8s + jenkins的部署架构 +* 稳定生产了一年,经历了从Camden -> Hoxton的技术架构,也经历了从fat jar -> docker -> k8s + jenkins的部署架构 * 项目分包明确,规范微服务的开发模式,使包与包之间的分工清晰。 ## 架构图 @@ -59,7 +59,8 @@ SpringBlade * 交流一群:`477853168`(满) * 交流二群:`751253339`(满) * 交流三群:`784729540`(满) -* 交流四群:`1034621754` +* 交流四群:`1034621754`(满) +* 交流五群:`946350912` ## 在线演示 * Saber-基于Vue:[https://saber.bladex.vip](https://saber.bladex.vip) diff --git a/config/router.config.js b/config/router.config.js index 5e7c708..25a60c8 100644 --- a/config/router.config.js +++ b/config/router.config.js @@ -15,7 +15,7 @@ export default [ path: '/', component: '../layouts/BasicLayout', Routes: ['src/pages/Authorized'], - authority: ['administrator', 'admin', 'user', 'test'], + authority: ['administrator', 'admin', 'user', 'test', 'guest'], routes: [ // dashboard { path: '/', redirect: '/dashboard/workplace' }, diff --git a/package.json b/package.json index 2553693..63489cc 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "sword", - "version": "2.7.0", + "version": "2.7.2", "description": "An out-of-box UI solution for enterprise applications", "private": true, "scripts": { "presite": "cd functions && npm install", - "start": "cross-env MOCK=none PORT=8888 umi dev", - "start:no-mock": "cross-env MOCK=none PORT=8888 umi dev", - "start:mock": "cross-env APP_TYPE=site PORT=8888 umi dev", + "start": "cross-env MOCK=none PORT=1888 umi dev", + "start:no-mock": "cross-env MOCK=none PORT=1888 umi dev", + "start:mock": "cross-env APP_TYPE=site PORT=1888 umi dev", "build": "umi build", "site": "npm run presite && cross-env APP_TYPE=site npm run build && firebase deploy && npm run docker:push", "analyze": "cross-env ANALYZE=1 umi build", diff --git a/src/components/Login/LoginItem.js b/src/components/Login/LoginItem.js index 6761f89..5e25847 100644 --- a/src/components/Login/LoginItem.js +++ b/src/components/Login/LoginItem.js @@ -47,7 +47,7 @@ class WrapFormItem extends Component { refreshCaptcha = () => { // 获取验证码 getCaptchaImage().then(resp => { - const {data} = resp; + const { data } = resp; if (data.key) { this.setState({ image: data.image }); setCaptchaKey(data.key); diff --git a/src/components/ThirdRegister/index.js b/src/components/ThirdRegister/index.js new file mode 100644 index 0000000..c57294e --- /dev/null +++ b/src/components/ThirdRegister/index.js @@ -0,0 +1,181 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Card, Col, Form, Input, Modal, Button, Row, message } from 'antd'; +import styles from '@/layouts/Sword.less'; +import { getCurrentUser, removeAll } from '@/utils/authority'; +import { validateNull } from '@/utils/utils'; +import { tenantMode } from '@/defaultSettings'; +import { getUserInfo, registerGuest } from '@/services/user'; +import router from 'umi/router'; + +const FormItem = Form.Item; + +@connect(({ tenant }) => ({ + tenant, +})) +@Form.create() +class ThirdRegister extends PureComponent { + state = { + loading: false, + visible: false, + user: {}, + }; + + componentDidMount() { + const user = getCurrentUser(); + if (validateNull(user) || validateNull(user.userId) || user.userId < 0) { + // 第三方注册用户,弹出注册框 + this.setState({ visible: true, user }); + } else { + // 获取用户信息,也可用于校验当前用户token是否有效 + getUserInfo().then(resp => { + window.console.log(resp); + }); + } + } + + handleSubmit = e => { + e.preventDefault(); + const { form } = this.props; + const user = getCurrentUser(); + form.validateFieldsAndScroll((err, values) => { + if (!err) { + const password = form.getFieldValue('password'); + const password2 = form.getFieldValue('password2'); + if (password !== password2) { + message.warning('两次密码输入不一致'); + } else { + registerGuest(values, user.oauthId).then(resp => { + if (resp.success) { + this.setState({ visible: false }); + Modal.success({ content: '注册申请已提交,请耐心等待管理员通过!' }); + removeAll(); + router.push('/user/login'); + } + form.resetFields(); + }); + } + } + }); + }; + + render() { + const { + form, + } = this.props; + + const { loading, visible, user } = this.state; + + const { getFieldDecorator } = form; + + const tenantVisible = tenantMode; + + const formItemLayout = { + labelCol: { + span: 8, + }, + wrapperCol: { + span: 16, + }, + }; + + const formAllItemLayout = { + labelCol: { + span: 4, + }, + wrapperCol: { + span: 20, + }, + }; + + return ( + + 注册 + , + ]} + > +

+ + {tenantVisible ? ( + + + + {getFieldDecorator('tenantId', { + rules: [ + { + required: true, + message: '请输入租户编号', + }, + ], + })()} + + + + ) : null} + + + + {getFieldDecorator('name', { + rules: [ + { + required: true, + message: '请输入用户姓名', + }, + ], + initialValue: user.name, + })()} + + + + + {getFieldDecorator('account', { + rules: [ + { + required: true, + message: '请输入账号名称', + }, + ], + initialValue: user.account, + })()} + + + + + + + {getFieldDecorator('password', { + rules: [ + { + required: true, + message: '请输入密码', + }, + ], + })()} + + + + + {getFieldDecorator('password2', { + rules: [ + { + required: true, + message: '请输入确认密码', + }, + ], + })()} + + + + +
+ + ); + } +} +export default ThirdRegister; diff --git a/src/defaultSettings.js b/src/defaultSettings.js index 81398ba..054a86a 100644 --- a/src/defaultSettings.js +++ b/src/defaultSettings.js @@ -16,4 +16,6 @@ module.exports = { disableLocal: false, }, pwa: true, + // 第三方登陆授权地址 + authUrl: 'http://localhost/blade-auth/oauth/render', }; diff --git a/src/locales/en-US/login.js b/src/locales/en-US/login.js index d1b6029..6a23299 100644 --- a/src/locales/en-US/login.js +++ b/src/locales/en-US/login.js @@ -6,6 +6,7 @@ export default { 'app.login.message-invalid-verification-code': 'Invalid verification code', 'app.login.tab-login-credentials': 'Credentials', 'app.login.tab-login-mobile': 'Mobile number', + 'app.login.tab-login-social': 'Social System', 'app.login.remember-me': 'Remember me', 'app.login.forgot-password': 'Forgot your password?', 'app.login.sign-in-with': 'Sign in with', diff --git a/src/locales/zh-CN/login.js b/src/locales/zh-CN/login.js index aae59fe..0dac0f2 100644 --- a/src/locales/zh-CN/login.js +++ b/src/locales/zh-CN/login.js @@ -6,6 +6,7 @@ export default { 'app.login.message-invalid-verification-code': '验证码错误', 'app.login.tab-login-credentials': '账户密码登录', 'app.login.tab-login-mobile': '手机号登录', + 'app.login.tab-login-social': '第三方系统登陆', 'app.login.remember-me': '自动登录', 'app.login.forgot-password': '忘记密码', 'app.login.sign-in-with': '其他登录方式', diff --git a/src/locales/zh-TW/login.js b/src/locales/zh-TW/login.js index 1ccf9ca..9a41519 100644 --- a/src/locales/zh-TW/login.js +++ b/src/locales/zh-TW/login.js @@ -6,6 +6,7 @@ export default { 'app.login.message-invalid-verification-code': '驗證碼錯誤', 'app.login.tab-login-credentials': '賬戶密碼登錄', 'app.login.tab-login-mobile': '手機號登錄', + 'app.login.tab-login-social': '第三方系統登陸', 'app.login.remember-me': '自動登錄', 'app.login.forgot-password': '忘記密碼', 'app.login.sign-in-with': '其他登錄方式', diff --git a/src/models/login.js b/src/models/login.js index dbeca4a..a8720ab 100644 --- a/src/models/login.js +++ b/src/models/login.js @@ -1,7 +1,8 @@ import { routerRedux } from 'dva/router'; +import { notification } from 'antd'; import { stringify } from 'qs'; import { getFakeCaptcha } from '../services/api'; -import { accountLogin } from '../services/user'; +import { accountLogin, socialLogin } from '../services/user'; import { dynamicRoutes, dynamicButtons } from '../services/menu'; import { setAuthority, @@ -14,6 +15,7 @@ import { } from '../utils/authority'; import { getPageQuery, formatRoutes, formatButtons } from '../utils/utils'; import { reloadAuthorized } from '../utils/Authorized'; +import { getTopUrl } from '../utils/utils'; export default { namespace: 'login', @@ -63,7 +65,29 @@ export default { yield put(routerRedux.replace(redirect || '/')); } }, - + *socialLogin({ payload }, { call, put }) { + const response = yield call(socialLogin, payload); + if (response.success) { + yield put({ + type: 'changeLoginStatus', + payload: { + status: true, + type: 'login', + data: { ...response.data }, + }, + }); + reloadAuthorized(); + const topUrl = getTopUrl(); + const redirectUrl = '/oauth/redirect/'; + // eslint-disable-next-line prefer-destructuring + window.location.href = topUrl.split(redirectUrl)[0]; + yield put(routerRedux.replace('/')); + } else { + notification.error({ + message: response.msg, + }); + } + }, *getCaptcha({ payload }, { call }) { yield call(getFakeCaptcha, payload); }, @@ -95,16 +119,15 @@ export default { reducers: { changeLoginStatus(state, { payload }) { const { status, type } = payload; - if (status) { const { - data: { tokenType, accessToken, authority, account, userName, avatar }, + data: { tokenType, accessToken, authority, account, userId, oauthId, userName, avatar }, } = payload; const token = `${tokenType} ${accessToken}`; setToken(token); setAccessToken(accessToken); setAuthority(authority); - setCurrentUser({ avatar, account, name: userName, authority }); + setCurrentUser({ avatar, userId, oauthId, account, name: userName, authority }); } else { removeAll(); } diff --git a/src/pages/Base/Region/Region.js b/src/pages/Base/Region/Region.js index da0a2b8..911de4c 100644 --- a/src/pages/Base/Region/Region.js +++ b/src/pages/Base/Region/Region.js @@ -236,11 +236,7 @@ class Region extends PureComponent { const buttons = getButton('region'); - const { - treeData, - treeCascader, - debugVisible, - } = this.state; + const { treeData, treeCascader, debugVisible } = this.state; const formItemLayout = { labelCol: { diff --git a/src/pages/Dashboard/Workplace.js b/src/pages/Dashboard/Workplace.js index dbf5814..b5c8bc9 100644 --- a/src/pages/Dashboard/Workplace.js +++ b/src/pages/Dashboard/Workplace.js @@ -2,7 +2,8 @@ import React, { PureComponent } from 'react'; import { Card, Col, Collapse, Row, Divider, Tag } from 'antd'; import styles from '../../layouts/Sword.less'; -import PageHeaderWrapper from '@/components/PageHeaderWrapper'; +import PageHeaderWrapper from '../../components/PageHeaderWrapper'; +import ThirdRegister from '../../components/ThirdRegister'; const { Panel } = Collapse; @@ -11,6 +12,11 @@ class Workplace extends PureComponent { return ( + + + + +
@@ -211,13 +217,15 @@ class Workplace extends PureComponent {
1.升级至 SpringCloud Hoxton.SR5
2.升级至 SpringBoot 2.2.7.RELEASE
3.升级至 Seata 1.2.0
-
4.升级至 FastJson 1.2.70
-
5.升级至 Avue 2.5.3
-
6.新增行政区划管理模块
-
7.优化用户导入的密码配置逻辑
-
8.优化INode结构支持懒加载数据格式
-
9.优化代码生成模板,支持最新版Saber结构
-
10.修复Log模块在多线程、异步场景下报错的问题
+
4.升级至 MybatisPlus 3.3.2
+
5.升级至 Kinfe4j 2.0.3
+
6.升级至 FastJson 1.2.70
+
7.升级至 Avue 2.5.3
+
8.新增行政区划管理模块
+
9.优化用户导入的密码配置逻辑
+
10.优化INode结构支持懒加载数据格式
+
11.优化代码生成模板,支持最新版Saber结构
+
12.修复Log模块在多线程、异步场景下报错的问题
1.升级至 SpringCloud Hoxton.SR3
diff --git a/src/pages/Login/Login.js b/src/pages/Login/Login.js index fef01a0..4244a50 100644 --- a/src/pages/Login/Login.js +++ b/src/pages/Login/Login.js @@ -1,10 +1,11 @@ import React, { Component } from 'react'; import { connect } from 'dva'; import { formatMessage, FormattedMessage } from 'umi/locale'; -import { Checkbox, Alert } from 'antd'; +import { Checkbox, Alert, Icon, Row, Col, Card, Spin } from 'antd'; import Login from '../../components/Login'; import styles from './Login.less'; -import { tenantMode, captchaMode } from '../../defaultSettings'; +import { tenantMode, captchaMode, authUrl } from '../../defaultSettings'; +import { getQueryString, getTopUrl, validateNull } from '@/utils/utils'; const { Tab, TenantId, UserName, Password, Captcha, Submit } = Login; @@ -18,6 +19,36 @@ class LoginPage extends Component { autoLogin: true, }; + componentDidMount() { + const domain = getTopUrl(); + const redirectUrl = '/oauth/redirect/'; + const { + dispatch, + route: { routes, authority }, + } = this.props; + + let source = getQueryString('source'); + const code = getQueryString('code'); + const state = getQueryString('state'); + if (validateNull(source) && domain.includes(redirectUrl)) { + // eslint-disable-next-line prefer-destructuring + source = domain.split('?')[0]; + // eslint-disable-next-line prefer-destructuring + source = source.split(redirectUrl)[1]; + } + if (!validateNull(source) && !validateNull(code) && !validateNull(state)) { + dispatch({ + type: 'login/socialLogin', + payload: { source, code, state, tenantId: '000000' }, + }); + } else { + dispatch({ + type: 'menu/fetchMenuData', + payload: { routes, authority }, + }); + } + } + onTabChange = type => { this.setState({ type }); }; @@ -53,6 +84,10 @@ class LoginPage extends Component { } }; + handleClick = source => { + window.location.href = `${authUrl}/${source}`; + }; + changeAutoLogin = e => { this.setState({ autoLogin: e.target.checked, @@ -65,7 +100,7 @@ class LoginPage extends Component { render() { const { login, submitting } = this.props; - const { type, autoLogin } = this.state; + const { type, autoLogin, loading } = this.state; return (
{captchaMode ? : null} + + + + + { + this.handleClick('github'); + }} + /> + + + { + this.handleClick('gitee'); + }} + /> + + + { + this.handleClick('wechat_open'); + }} + /> + + + { + this.handleClick('dingtalk'); + }} + /> + + + { + this.handleClick('alipay'); + }} + /> + + + { + this.handleClick('taobao'); + }} + /> + + + +
diff --git a/src/pages/Login/Login.less b/src/pages/Login/Login.less index 8eba81c..e4a9037 100644 --- a/src/pages/Login/Login.less +++ b/src/pages/Login/Login.less @@ -29,4 +29,14 @@ float: right; } } + + .card { + margin-bottom: 24px; + } + + .iconPreview { + font-size: 45px; + text-align: center; + cursor: pointer; + } } diff --git a/src/pages/System/User/User.js b/src/pages/System/User/User.js index c44d59f..8b0e34a 100644 --- a/src/pages/System/User/User.js +++ b/src/pages/System/User/User.js @@ -1,17 +1,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'dva'; -import { - Upload, - Icon, - Button, - Col, - Form, - Input, - message, - Modal, - Row, - Tree, -} from 'antd'; +import { Upload, Icon, Button, Col, Form, Input, message, Modal, Row, Tree } from 'antd'; import Panel from '../../../components/Panel'; import Grid from '../../../components/Sword/Grid'; import { USER_INIT, USER_LIST, USER_ROLE_GRANT } from '../../../actions/user'; @@ -249,7 +238,6 @@ class User extends PureComponent {
); - render() { const code = 'user'; @@ -269,7 +257,7 @@ class User extends PureComponent { headers: { 'Blade-Auth': getToken(), }, - action: "/api/blade-user/import-user", + action: '/api/blade-user/import-user', }; const formItemLayout = { diff --git a/src/pages/System/User/UserEdit.js b/src/pages/System/User/UserEdit.js index ce04bb0..e469c69 100644 --- a/src/pages/System/User/UserEdit.js +++ b/src/pages/System/User/UserEdit.js @@ -23,7 +23,7 @@ class UserEdit extends PureComponent { params: { id }, }, } = this.props; - dispatch(USER_DETAIL(id)).then(()=>{ + dispatch(USER_DETAIL(id)).then(() => { const { user: { detail }, } = this.props; @@ -227,7 +227,7 @@ class UserEdit extends PureComponent { {getFieldDecorator('code', { - initialValue: detail.code + initialValue: detail.code, })()} diff --git a/src/services/user.js b/src/services/user.js index 377a5af..8180a91 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -20,6 +20,25 @@ export async function accountLogin(params) { }); } +export async function socialLogin(params) { + const values = params; + values.grantType = 'social'; + values.scope = 'all'; + return request('/api/blade-auth/token', { + method: 'POST', + body: func.toFormData(values), + }); +} + +export async function registerGuest(form, oauthId) { + const values = form; + values.oauthId = oauthId; + return request('/api/blade-user/register-guest', { + method: 'POST', + body: func.toFormData(values), + }); +} + export async function query() { return request('/api/users'); } diff --git a/src/utils/Func.js b/src/utils/Func.js index 9c427ec..e9855cd 100644 --- a/src/utils/Func.js +++ b/src/utils/Func.js @@ -88,6 +88,9 @@ export default class Func { * @returns {string} */ static split(str) { + if (String(str) === '-1') { + return null; + } return str ? String(str).split(',') : ''; } } diff --git a/src/utils/getPageTitle.js b/src/utils/getPageTitle.js new file mode 100644 index 0000000..0dd1e62 --- /dev/null +++ b/src/utils/getPageTitle.js @@ -0,0 +1,27 @@ +import { formatMessage } from 'umi/locale'; +import pathToRegexp from 'path-to-regexp'; +import isEqual from 'lodash/isEqual'; +import memoizeOne from 'memoize-one'; +import { menu, title } from '../defaultSettings'; + +export const matchParamsPath = (pathname, breadcrumbNameMap) => { + const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname)); + return breadcrumbNameMap[pathKey]; +}; + +const getPageTitle = (pathname, breadcrumbNameMap) => { + const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); + if (!currRouterData) { + return title; + } + const pageName = menu.disableLocal + ? currRouterData.name + : formatMessage({ + id: currRouterData.locale || currRouterData.name, + defaultMessage: currRouterData.name, + }); + + return `${pageName} - ${title}`; +}; + +export default memoizeOne(getPageTitle, isEqual); diff --git a/src/utils/utils.js b/src/utils/utils.js index cdd87fd..bbaf03c 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -212,3 +212,45 @@ export function formatButtons(buttons) { }; }); } + +/** + * 判断是否为空 + */ +export function validateNull(val) { + if (typeof val === 'boolean') { + return false; + } + if (typeof val === 'number') { + return false; + } + if (val instanceof Array) { + if (val.length === 0) return true; + } else if (val instanceof Object) { + if (JSON.stringify(val) === '{}') return true; + } else { + if (val === 'null' || val == null || val === 'undefined' || val === undefined || val === '') { + return true; + } + return false; + } + return false; +} + +/** + * 获取顶部地址栏地址 + */ +export function getTopUrl() { + return window.location.href.split('/#/')[0]; +} + +/** + * 获取url参数 + * @param name 参数名 + */ +export function getQueryString(name) { + // eslint-disable-next-line no-shadow + const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i'); + const r = window.location.search.substr(1).match(reg); + if (r != null) return unescape(decodeURI(r[2])); + return null; +}