From 94375485291eeb0c327220f2bfb3965a208b1404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 03:10:55 +0800 Subject: [PATCH 1/8] Install react-router-redux --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 838e01d..f83ba0a 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "react-redux": "^4.4.5", "react-router": "^2.3.0", "react-router-active-component": "^4.0.0", + "react-router-redux": "^4.0.6", "redux": "^3.5.2", "redux-form": "^6.1.0", "redux-thunk": "^2.1.0", From 065389f29526b517f6f6bbb87b282e243b459bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 03:15:18 +0800 Subject: [PATCH 2/8] Apply react-router-redux into client side --- src/client/index.js | 18 +++++++++++++++--- src/common/reducers/index.js | 2 ++ src/common/reducers/routerReducer.js | 2 ++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/common/reducers/routerReducer.js diff --git a/src/client/index.js b/src/client/index.js index 75513bb..4a1d945 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -4,6 +4,10 @@ import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; import thunk from 'redux-thunk'; import { match, Router, browserHistory } from 'react-router'; +import { + routerMiddleware, + syncHistoryWithStore, +} from 'react-router-redux'; import LocaleProvider from '../common/components/utils/LocaleProvider'; import rootReducer from '../common/reducers'; import getRoutes from '../common/routes'; @@ -17,7 +21,14 @@ setupNProgress(); setupLocale(); let logPageView = setupGA(); const initialState = window.__INITIAL_STATE__; -let store = createStore(rootReducer, initialState, applyMiddleware(thunk)); +let store = createStore( + rootReducer, + initialState, + applyMiddleware( + routerMiddleware(browserHistory), + thunk + ) +); let apiEngine = new ApiEngine(); store.dispatch(setApiEngine(apiEngine)); @@ -25,9 +36,10 @@ store.dispatch(setApiEngine(apiEngine)); // refs: // - // - +let history = syncHistoryWithStore(browserHistory, store); let routes = getRoutes(store); match({ - history: browserHistory, + history, routes, }, (error, redirectLocation, renderProps) => { if (error) { @@ -37,7 +49,7 @@ match({ diff --git a/src/common/reducers/index.js b/src/common/reducers/index.js index eec2784..f62f40c 100644 --- a/src/common/reducers/index.js +++ b/src/common/reducers/index.js @@ -1,4 +1,5 @@ import { combineReducers } from 'redux'; +import routing from './routerReducer'; import cookies from './cookieReducer'; import errors from './errorReducer'; import apiEngine from './apiEngineReducer'; @@ -8,6 +9,7 @@ import form from './formReducer'; import intl from './intlReducer'; const rootReducer = combineReducers({ + routing, cookies, errors, apiEngine, diff --git a/src/common/reducers/routerReducer.js b/src/common/reducers/routerReducer.js new file mode 100644 index 0000000..8415b35 --- /dev/null +++ b/src/common/reducers/routerReducer.js @@ -0,0 +1,2 @@ +import { routerReducer } from 'react-router-redux'; +export default routerReducer; From 5ead7824dd7b4a3825f693fdbda60b572290db08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 03:21:02 +0800 Subject: [PATCH 3/8] Apply react-router-redux into server side --- src/server/controllers/react.js | 3 ++- src/server/middlewares/mountStore.js | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/server/controllers/react.js b/src/server/controllers/react.js index f21ded0..da83691 100644 --- a/src/server/controllers/react.js +++ b/src/server/controllers/react.js @@ -15,7 +15,8 @@ export default { let routes = getRoutes(req.store); match({ routes, - location: req.url, + // we use `history: req.history` instead of `location: req.url` to deal with redirections + history: req.history, }, (error, redirectLocation, renderProps) => { if (error) { return res.status(500).send(error.message); diff --git a/src/server/middlewares/mountStore.js b/src/server/middlewares/mountStore.js index a11d168..d000146 100644 --- a/src/server/middlewares/mountStore.js +++ b/src/server/middlewares/mountStore.js @@ -1,12 +1,27 @@ import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; +import { useRouterHistory, createMemoryHistory } from 'react-router'; +import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'; import rootReducer from '../../common/reducers'; import ApiEngine from '../../common/utils/ApiEngine'; import { setApiEngine } from '../../common/actions/apiEngine'; export default (req, res, next) => { - const store = createStore(rootReducer, applyMiddleware(thunk)); + // ref: + // - + // - + // - + let memoryHistory = useRouterHistory(createMemoryHistory)(req.url); + let store = createStore( + rootReducer, + applyMiddleware( + routerMiddleware(memoryHistory), + thunk + ) + ); + let history = syncHistoryWithStore(memoryHistory, store); req.store = store; + req.history = history; let apiEngine = new ApiEngine(req); req.store.dispatch(setApiEngine(apiEngine)); next(); From 3ef7edd6fc4e6468137bf5dd186c0107fc072620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 03:24:52 +0800 Subject: [PATCH 4/8] Add redirection mechanism --- src/client/index.js | 8 ++++++++ src/common/actions/routeActions.js | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/common/actions/routeActions.js diff --git a/src/client/index.js b/src/client/index.js index 4a1d945..1813938 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -7,6 +7,7 @@ import { match, Router, browserHistory } from 'react-router'; import { routerMiddleware, syncHistoryWithStore, + push, } from 'react-router-redux'; import LocaleProvider from '../common/components/utils/LocaleProvider'; import rootReducer from '../common/reducers'; @@ -15,6 +16,7 @@ import setupLocale from './setupLocale'; import setupNProgress from './setupNProgress'; import setupGA from './setupGA'; import { setApiEngine } from '../common/actions/apiEngine'; +import { removeCookie } from '../common/actions/cookieActions'; import ApiEngine from '../common/utils/ApiEngine'; setupNProgress(); @@ -33,6 +35,12 @@ let store = createStore( let apiEngine = new ApiEngine(); store.dispatch(setApiEngine(apiEngine)); +let { redirect } = store.getState().cookies; +if (redirect) { + store.dispatch(push(redirect)); + store.dispatch(removeCookie('redirect')); +} + // refs: // - // - diff --git a/src/common/actions/routeActions.js b/src/common/actions/routeActions.js new file mode 100644 index 0000000..64292ee --- /dev/null +++ b/src/common/actions/routeActions.js @@ -0,0 +1,9 @@ +import { push } from 'react-router-redux'; +import { setCookie } from './cookieActions'; + +export const redirect = (path) => { + return (dispatch) => { + dispatch(setCookie('redirect', path)); + dispatch(push(path)); + }; +}; From 23125102251edc62034a21dd7f7eafe99b4f38d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 03:47:44 +0800 Subject: [PATCH 5/8] Update page transition mechanism --- src/common/components/forms/LoginForm.js | 28 ++++++++++--------- src/common/components/forms/RegisterForm.js | 21 +++++++------- .../components/pages/admin/user/ListPage.js | 12 ++++---- src/common/components/pages/todo/ListPage.js | 12 ++++---- .../components/pages/user/LogoutPage.js | 19 +++++++------ 5 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/common/components/forms/LoginForm.js b/src/common/components/forms/LoginForm.js index 95a907b..daf8023 100644 --- a/src/common/components/forms/LoginForm.js +++ b/src/common/components/forms/LoginForm.js @@ -1,4 +1,6 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; import { Field, reduxForm } from 'redux-form'; import Button from 'react-bootstrap/lib/Button'; // import validator from 'validator'; @@ -33,17 +35,20 @@ class LoginForm extends Component { } _login(json) { - return this.context.store.dispatch(loginUser({ + return this.props.dispatch(loginUser({ token: json.token, data: json.user, })); } _handleSubmit(formData) { - return userAPI(this.context.store.getState().apiEngine) + // let { store } = this.context; + let { dispatch, apiEngine } = this.props; + + return userAPI(apiEngine) .login(formData) .catch((err) => { - this.context.store.dispatch(pushErrors(err)); + dispatch(pushErrors(err)); throw err; }) .then((json) => { @@ -52,13 +57,13 @@ class LoginForm extends Component { // redirect to the origin path before logging in const { location } = this.props; if (location && location.state && location.state.nextPathname) { - this.context.router.push(location.state.nextPathname); + dispatch(push(location.state.nextPathname)); } else { - this.context.router.push('/'); + dispatch(push('/')); } }); } else { - this.context.store.dispatch(pushErrors([{ + dispatch(pushErrors([{ title: 'User Not Exists', detail: 'You may type wrong email or password.', }])); @@ -100,12 +105,9 @@ class LoginForm extends Component { } }; -LoginForm.contextTypes = { - store: PropTypes.object.isRequired, - router: PropTypes.any.isRequired, -}; - export default reduxForm({ form: 'login', validate, -})(LoginForm); +})(connect(state => ({ + apiEngine: state.apiEngine, +}))(LoginForm)); diff --git a/src/common/components/forms/RegisterForm.js b/src/common/components/forms/RegisterForm.js index eeab539..cb9d87f 100644 --- a/src/common/components/forms/RegisterForm.js +++ b/src/common/components/forms/RegisterForm.js @@ -1,4 +1,6 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; import { Field, reduxForm } from 'redux-form'; import Button from 'react-bootstrap/lib/Button'; // import validator from 'validator'; @@ -43,14 +45,16 @@ class RegisterForm extends Component { } _handleSubmit(formData) { - return userAPI(this.context.store.getState().apiEngine) + let { dispatch, apiEngine } = this.props; + + return userAPI(apiEngine) .register(formData) .catch((err) => { - this.context.store.dispatch(pushErrors(err)); + dispatch(pushErrors(err)); throw err; }) .then((json) => { - this.context.router.push('/'); + dispatch(push('/')); }); } @@ -99,14 +103,11 @@ class RegisterForm extends Component { } }; -RegisterForm.contextTypes = { - store: PropTypes.any.isRequired, - router: PropTypes.any.isRequired, -}; - export default reduxForm({ form: 'register', validate, asyncValidate, asyncBlurFields: ['email'], -})(RegisterForm); +})(connect(state => ({ + apiEngine: state.apiEngine, +}))(RegisterForm)); diff --git a/src/common/components/pages/admin/user/ListPage.js b/src/common/components/pages/admin/user/ListPage.js index 35149d3..daa501a 100644 --- a/src/common/components/pages/admin/user/ListPage.js +++ b/src/common/components/pages/admin/user/ListPage.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; +import { push } from 'react-router-redux'; import PageHeader from 'react-bootstrap/lib/PageHeader'; import Table from 'react-bootstrap/lib/Table'; import Resources from '../../../../constants/Resources'; @@ -24,7 +24,7 @@ class ListPage extends Component { } componentDidUpdate(prevProps) { - let { dispatch, apiEngine, page, router, location } = this.props; + let { dispatch, apiEngine, page, location } = this.props; if (prevProps.page.current !== page.current) { userAPI(apiEngine) @@ -36,10 +36,10 @@ class ListPage extends Component { .then((json) => { this.setState({ users: json.users }); dispatch(setPage(Resources.USER, json.page)); - router.push({ + dispatch(push({ pathname: location.pathname, query: { page: json.page.current }, - }); + })); }); } } @@ -78,7 +78,7 @@ class ListPage extends Component { } } -export default withRouter(connect(state => ({ +export default connect(state => ({ apiEngine: state.apiEngine, page: state.pages[Resources.USER] || {}, -}))(ListPage)); +}))(ListPage); diff --git a/src/common/components/pages/todo/ListPage.js b/src/common/components/pages/todo/ListPage.js index 5ed075c..a4ee00c 100644 --- a/src/common/components/pages/todo/ListPage.js +++ b/src/common/components/pages/todo/ListPage.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; +import { push } from 'react-router-redux'; import PageHeader from 'react-bootstrap/lib/PageHeader'; import Resources from '../../../constants/Resources'; import todoAPI from '../../../api/todo'; @@ -93,7 +93,7 @@ class ListPage extends Component { } componentDidUpdate(prevProps) { - let { dispatch, apiEngine, page, router, location } = this.props; + let { dispatch, apiEngine, page, location } = this.props; if (prevProps.page.current !== page.current) { todoAPI(apiEngine) @@ -105,10 +105,10 @@ class ListPage extends Component { .then((json) => { dispatch(setTodo(json.todos)); dispatch(setPage(Resources.TODO, json.page)); - router.push({ + dispatch(push({ pathname: location.pathname, query: { page: json.page.current }, - }); + })); }); } } @@ -177,8 +177,8 @@ class ListPage extends Component { } }; -export default withRouter(connect(state => ({ +export default connect(state => ({ apiEngine: state.apiEngine, todos: state.todos, page: state.pages[Resources.TODO] || {}, -}))(ListPage)); +}))(ListPage); diff --git a/src/common/components/pages/user/LogoutPage.js b/src/common/components/pages/user/LogoutPage.js index 915e64f..8993ccc 100644 --- a/src/common/components/pages/user/LogoutPage.js +++ b/src/common/components/pages/user/LogoutPage.js @@ -1,4 +1,6 @@ import React from 'react'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; import userAPI from '../../../api/user'; import { logoutUser } from '../../../actions/userActions'; @@ -9,11 +11,13 @@ class LogoutPage extends React.Component { } _logout() { - this.context.store.dispatch(logoutUser()); + this.props.dispatch(logoutUser()); } componentWillMount() { - userAPI(this.context.store.getState().apiEngine) + let { dispatch, apiEngine } = this.props; + + userAPI(apiEngine) .logout() .catch((err) => { alert('Logout user fail'); @@ -21,7 +25,7 @@ class LogoutPage extends React.Component { }) .then((json) => { this._logout(); - this.context.router.push('/'); + dispatch(push('/')); }); } @@ -30,9 +34,6 @@ class LogoutPage extends React.Component { } }; -LogoutPage.contextTypes = { - store: React.PropTypes.object.isRequired, - router: React.PropTypes.any.isRequired, -}; - -export default LogoutPage; +export default connect(state => ({ + apiEngine: state.apiEngine, +}))(LogoutPage); From 1a9d02702df66439f514bf8ec058fd479fa324af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 14:42:28 +0800 Subject: [PATCH 6/8] Route to controlled next path after local login --- src/common/components/forms/LoginForm.js | 9 +++------ src/common/utils/authRequired.js | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/common/components/forms/LoginForm.js b/src/common/components/forms/LoginForm.js index daf8023..05c5066 100644 --- a/src/common/components/forms/LoginForm.js +++ b/src/common/components/forms/LoginForm.js @@ -55,12 +55,8 @@ class LoginForm extends Component { if (json.isAuth) { this.login(json).then(() => { // redirect to the origin path before logging in - const { location } = this.props; - if (location && location.state && location.state.nextPathname) { - dispatch(push(location.state.nextPathname)); - } else { - dispatch(push('/')); - } + let { next } = this.props.routing.locationBeforeTransitions.query; + dispatch(push(next || '/')); }); } else { dispatch(pushErrors([{ @@ -110,4 +106,5 @@ export default reduxForm({ validate, })(connect(state => ({ apiEngine: state.apiEngine, + routing: state.routing, }))(LoginForm)); diff --git a/src/common/utils/authRequired.js b/src/common/utils/authRequired.js index 823b461..25eb3d9 100644 --- a/src/common/utils/authRequired.js +++ b/src/common/utils/authRequired.js @@ -4,8 +4,8 @@ export default (store) => (nextState, replace) => { if (!token) { replace({ pathname: '/user/login', - state: { - nextPathname: nextState.location.pathname, + query: { + next: nextState.location.pathname, }, }); } From 8dcb7d62285766d6f83966c71bfe6443b502af75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 14:50:16 +0800 Subject: [PATCH 7/8] Route to controlled next path after social login --- src/common/components/pages/user/LoginPage.js | 67 ++++++++++--------- src/server/controllers/socialAuth.js | 20 ++++-- src/server/controllers/user.js | 5 +- 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/common/components/pages/user/LoginPage.js b/src/common/components/pages/user/LoginPage.js index 16699a7..65dfef4 100644 --- a/src/common/components/pages/user/LoginPage.js +++ b/src/common/components/pages/user/LoginPage.js @@ -7,36 +7,41 @@ import PageLayout from '../../layouts/PageLayout'; import Head from '../../widgets/Head'; import LoginForm from '../../forms/LoginForm'; -const LoginPage = (props) => ( - - - Login - - - - - - - - Login with Facebook - - - Login with LinkedIn - - - - - -); +let LoginPage = ({ location }) => { + let { next } = location.query; + let search = next ? '?next=' + next : ''; + + return ( + + + + Login + + + + + + + Login with Facebook + + + Login with LinkedIn + + + + + + ); +}; export default LoginPage; diff --git a/src/server/controllers/socialAuth.js b/src/server/controllers/socialAuth.js index d456712..5b59418 100644 --- a/src/server/controllers/socialAuth.js +++ b/src/server/controllers/socialAuth.js @@ -1,10 +1,18 @@ import passport from 'passport'; export default { - initFacebook: passport.authenticate('facebook', { - scope: ['public_profile', 'email'], - }), - initLinkedin: passport.authenticate('linkedin', { - state: Math.random(), - }), + initFacebook: (req, res, next) => ( + passport.authenticate('facebook', { + scope: ['public_profile', 'email'], + state: JSON.stringify({ next: req.query.next }), + })(req, res, next) + ), + initLinkedin: (req, res, next) => ( + passport.authenticate('linkedin', { + state: JSON.stringify({ + next: req.query.next, + random: Math.random(), + }), + })(req, res, next) + ), }; diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index 697d052..9c62636 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -3,6 +3,7 @@ import { handleDbError } from '../decorators/handleError'; import User from '../models/User'; import filterAttribute from '../utils/filterAttribute'; import { loginUser } from '../../common/actions/userActions'; +import { redirect } from '../../common/actions/routeActions'; export default { list(req, res) { @@ -87,10 +88,12 @@ export default { })) .then(() => { let { token, user } = req.store.getState().cookies; + let state = JSON.parse(req.query.state); res.cookie('token', token); res.cookie('user', user); - res.redirect('/'); + req.store.dispatch(redirect(state.next || '/')); + return next(); }); })); }, From 9e79ce5a4bacf446f54952f34a4ca098d0363935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=81=E6=B2=BB=E5=B9=B3?= Date: Mon, 24 Oct 2016 14:59:55 +0800 Subject: [PATCH 8/8] Update UI --- src/common/components/layouts/PageLayout.js | 20 ++++++++++++++----- src/common/components/pages/user/LoginPage.js | 13 +++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/common/components/layouts/PageLayout.js b/src/common/components/layouts/PageLayout.js index e27bee2..1012ba0 100644 --- a/src/common/components/layouts/PageLayout.js +++ b/src/common/components/layouts/PageLayout.js @@ -1,16 +1,26 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import Grid from 'react-bootstrap/lib/Grid'; import Navigation from '../utils/Navigation'; import ErrorList from '../utils/ErrorList'; -const PageLayout = ({ children, ...rest }) => ( +let PageLayout = ({ hasGrid, children, ...rest }) => (
- - {children} - + {hasGrid ? ( + + {children} + + ) : children}
); +PageLayout.propTypes = { + hasGrid: PropTypes.bool, +}; + +PageLayout.defaultProps = { + hasGrid: true, +}; + export default PageLayout; diff --git a/src/common/components/pages/user/LoginPage.js b/src/common/components/pages/user/LoginPage.js index 65dfef4..78708f7 100644 --- a/src/common/components/pages/user/LoginPage.js +++ b/src/common/components/pages/user/LoginPage.js @@ -1,5 +1,6 @@ import React from 'react'; import PageHeader from 'react-bootstrap/lib/PageHeader'; +import Alert from 'react-bootstrap/lib/Alert'; import Grid from 'react-bootstrap/lib/Grid'; import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; @@ -12,7 +13,7 @@ let LoginPage = ({ location }) => { let search = next ? '?next=' + next : ''; return ( - + { /> Login + + + {next && ( + + Authentication Required + {' '}Please login first. + + )} + +