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", diff --git a/src/client/index.js b/src/client/index.js index 75513bb..1813938 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -4,6 +4,11 @@ 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, + push, +} from 'react-router-redux'; import LocaleProvider from '../common/components/utils/LocaleProvider'; import rootReducer from '../common/reducers'; import getRoutes from '../common/routes'; @@ -11,23 +16,38 @@ 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(); 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)); +let { redirect } = store.getState().cookies; +if (redirect) { + store.dispatch(push(redirect)); + store.dispatch(removeCookie('redirect')); +} + // refs: // - // - +let history = syncHistoryWithStore(browserHistory, store); let routes = getRoutes(store); match({ - history: browserHistory, + history, routes, }, (error, redirectLocation, renderProps) => { if (error) { @@ -37,7 +57,7 @@ match({ 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)); + }; +}; diff --git a/src/common/components/forms/LoginForm.js b/src/common/components/forms/LoginForm.js index 95a907b..05c5066 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,32 +35,31 @@ 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) => { 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) { - this.context.router.push(location.state.nextPathname); - } else { - this.context.router.push('/'); - } + let { next } = this.props.routing.locationBeforeTransitions.query; + dispatch(push(next || '/')); }); } else { - this.context.store.dispatch(pushErrors([{ + dispatch(pushErrors([{ title: 'User Not Exists', detail: 'You may type wrong email or password.', }])); @@ -100,12 +101,10 @@ 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, + routing: state.routing, +}))(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/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/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/LoginPage.js b/src/common/components/pages/user/LoginPage.js index 16699a7..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'; @@ -7,36 +8,51 @@ 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 + + + {next && ( + + Authentication Required + {' '}Please login first. + + )} + + + + + + + + + Login with Facebook + + + Login with LinkedIn + + + + + + ); +}; export default LoginPage; 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); 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; 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, }, }); } 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/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(); }); })); }, 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();