diff --git a/README.md b/README.md index 4545b9b..1bbaadd 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' import { Router, Route } from 'react-router' import { createHistory } from 'history' -import { syncReduxAndRouter, routeReducer } from 'redux-simple-router' +import { syncReduxAndRouter, routeReducer, routeActions } from 'redux-simple-router' import { UserAuthWrapper } from 'redux-auth-wrapper' import userReducer from '/reducers/userReducer' @@ -38,8 +38,9 @@ routingMiddleware.listenForReplays(store) // Redirects to /login by default const UserIsAuthenticated = UserAuthWrapper({ - authSelector: state => state.user, - wrapperDisplayName: 'UserIsAuthenticated' + authSelector: state => state.user, // how to get the user state + redirectAction: routeActions.replace, // the redux action to dispatch for redirect + wrapperDisplayName: 'UserIsAuthenticated' // a nice name for this auth check }) ReactDOM.render( @@ -90,6 +91,7 @@ Any time the user data changes, the UserAuthWrapper will re-check for authentica * `authSelector(state, [ownProps]): authData` \(*Function*): A state selector for the auth data. Just like `mapToStateProps` * `[failureRedirectPath]` \(*String*): Optional path to redirect the browser to on a failed check. Defaults to `/login` +* `[redirectAction]` \(*Function*): Optional redux action creator for redirecting the user. If not present, will use React-Router's router context to perform the transition. * `[wrapperDisplayName]` \(*String*): Optional name describing this authentication or authorization check. It will display in React-devtools. Defaults to `UserAuthWrapper` * `[predicate(authData): Bool]` \(*Function*): Optional function to be passed the result of the `userAuthSelector` param. @@ -105,6 +107,7 @@ If it evaluates to false the browser will be redirected to `failureRedirectPath` /* Allow only users with first name Bob */ const OnlyBob = UserAuthWrapper({ authSelector: state => state.user, + redirectAction: routeActions.replace, failureRedirectPath: '/app', wrapperDisplayName: 'UserIsOnlyBob', predicate: user => user.firstName === 'Bob' @@ -115,11 +118,13 @@ const OnlyBob = UserAuthWrapper({ // Take the regular authentication & redirect to login from before const UserIsAuthenticated = UserAuthWrapper({ authSelector: state => state.user, + redirectAction: routeActions.replace, wrapperDisplayName: 'UserIsAuthenticated' }) // Admin Authorization, redirects non-admins to /app and don't send a redirect param const UserIsAdmin = UserAuthWrapper({ authSelector: state => state.user, + redirectAction: routeActions.replace, failureRedirectPath: '/app', wrapperDisplayName: 'UserIsAdmin', predicate: user => user.isAdmin, diff --git a/examples/basic/app.js b/examples/basic/app.js index abff897..632d10b 100644 --- a/examples/basic/app.js +++ b/examples/basic/app.js @@ -7,7 +7,7 @@ import ReactDOM from 'react-dom' import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import { Provider } from 'react-redux' import { Router, Route, IndexRoute, browserHistory } from 'react-router' -import { syncHistory, routeReducer } from 'redux-simple-router' +import { syncHistory, routeReducer, routeActions } from 'redux-simple-router' import { UserAuthWrapper } from 'redux-auth-wrapper' import * as reducers from './reducers' @@ -35,10 +35,12 @@ routingMiddleware.listenForReplays(store) const UserIsAuthenticated = UserAuthWrapper({ authSelector: state => state.user, + redirectAction: routeActions.replace, wrapperDisplayName: 'UserIsAuthenticated' }) const UserIsAdmin = UserAuthWrapper({ authSelector: state => state.user, + redirectAction: routeActions.replace, failureRedirectPath: '/', wrapperDisplayName: 'UserIsAdmin', predicate: user => user.isAdmin, diff --git a/src/index.js b/src/index.js index 9a5349b..36fb4ec 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,4 @@ import React, { Component, PropTypes } from 'react' -import { routeActions } from 'redux-simple-router' import { connect } from 'react-redux' import hoistStatics from 'hoist-non-react-statics' import isEmpty from 'lodash.isempty' @@ -13,7 +12,7 @@ const defaults = { } const UserAuthWrapper = (args) => { - const { authSelector, failureRedirectPath, wrapperDisplayName, predicate, allowRedirectBack } = { + const { authSelector, failureRedirectPath, wrapperDisplayName, predicate, allowRedirectBack, redirectAction } = { ...defaults, ...args } @@ -21,9 +20,17 @@ const UserAuthWrapper = (args) => { return function wrapComponent(DecoratedComponent) { const displayName = DecoratedComponent.displayName || DecoratedComponent.name || 'Component' + const mapDispatchToProps = (dispatch) => { + if (redirectAction !== undefined) { + return { redirect: (args) => dispatch(redirectAction(args)) } + } else { + return {} + } + } + @connect( state => { return { authData: authSelector(state) } }, - { replace: routeActions.replace } + mapDispatchToProps, ) class UserAuthWrapper extends Component { @@ -34,10 +41,15 @@ const UserAuthWrapper = (args) => { pathname: PropTypes.string.isRequired, search: PropTypes.string.isRequired }).isRequired, - replace: PropTypes.func.isRequired, + redirect: PropTypes.func, authData: PropTypes.object }; + static contextTypes = { + // Only used if no redirectAction specified + router: React.PropTypes.object.isRequired + }; + componentWillMount() { this.ensureLoggedIn(this.props) } @@ -46,10 +58,12 @@ const UserAuthWrapper = (args) => { this.ensureLoggedIn(nextProps) } + getRedirectFunc = () => this.props.redirect || this.context.router.replace; + isAuthorized = (authData) => predicate(authData); ensureLoggedIn = (props) => { - const { replace, location, authData } = props + const { location, authData } = props let query if (allowRedirectBack) { query = { redirect: `${location.pathname}${location.search}` } @@ -58,7 +72,7 @@ const UserAuthWrapper = (args) => { } if (!this.isAuthorized(authData)) { - replace({ + this.getRedirectFunc()({ pathname: failureRedirectPath, query }) @@ -68,7 +82,7 @@ const UserAuthWrapper = (args) => { render() { // Allow everything but the replace aciton creator to be passed down // Includes route props from React-Router and authData - const { replace, authData, ...otherProps } = this.props + const { redirect, authData, ...otherProps } = this.props if (this.isAuthorized(authData)) { return diff --git a/test/UserAuthWrapper-test.js b/test/UserAuthWrapper-test.js index dde307a..af473d9 100644 --- a/test/UserAuthWrapper-test.js +++ b/test/UserAuthWrapper-test.js @@ -5,7 +5,7 @@ import { Provider } from 'react-redux' import { applyMiddleware, createStore, combineReducers, compose } from 'redux' import { renderIntoDocument, findRenderedComponentWithType } from 'react-addons-test-utils' import createMemoryHistory from 'react-router/lib/createMemoryHistory' -import { routeReducer, syncHistory } from 'redux-simple-router' +import { routeReducer, syncHistory, routeActions } from 'redux-simple-router' import { UserAuthWrapper } from '../src' @@ -41,11 +41,13 @@ const userSelector = state => state.user const UserIsAuthenticated = UserAuthWrapper({ authSelector: userSelector, + redirectAction: routeActions.replace, wrapperDisplayName: 'UserIsAuthenticated' }) const HiddenNoRedir = UserAuthWrapper({ authSelector: userSelector, + redirectAction: routeActions.replace, failureRedirectPath: '/', wrapperDisplayName: 'NoRedir', predicate: () => false, @@ -54,6 +56,7 @@ const HiddenNoRedir = UserAuthWrapper({ const UserIsOnlyTest = UserAuthWrapper({ authSelector: userSelector, + redirectAction: routeActions.replace, failureRedirectPath: '/', wrapperDisplayName: 'UserIsOnlyTest', predicate: user => user.firstName === 'Test'