Skip to content

Commit

Permalink
Merge pull request #13 from mjrussell/redirect-action-config
Browse files Browse the repository at this point in the history
Create redirect action config prop to support any redux router integration
  • Loading branch information
mjrussell committed Feb 3, 2016
2 parents a0f88ee + 74d500e commit 6eaa14b
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 12 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<project-path>/reducers/userReducer'

Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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'
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion examples/basic/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 21 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -13,17 +12,25 @@ const defaults = {
}

const UserAuthWrapper = (args) => {
const { authSelector, failureRedirectPath, wrapperDisplayName, predicate, allowRedirectBack } = {
const { authSelector, failureRedirectPath, wrapperDisplayName, predicate, allowRedirectBack, redirectAction } = {
...defaults,
...args
}
// Wraps the component that needs the auth enforcement
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 {

Expand All @@ -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)
}
Expand All @@ -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}` }
Expand All @@ -58,7 +72,7 @@ const UserAuthWrapper = (args) => {
}

if (!this.isAuthorized(authData)) {
replace({
this.getRedirectFunc()({
pathname: failureRedirectPath,
query
})
Expand All @@ -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 <DecoratedComponent authData={authData} {...otherProps} />
Expand Down
5 changes: 4 additions & 1 deletion test/UserAuthWrapper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,
Expand All @@ -54,6 +56,7 @@ const HiddenNoRedir = UserAuthWrapper({

const UserIsOnlyTest = UserAuthWrapper({
authSelector: userSelector,
redirectAction: routeActions.replace,
failureRedirectPath: '/',
wrapperDisplayName: 'UserIsOnlyTest',
predicate: user => user.firstName === 'Test'
Expand Down

0 comments on commit 6eaa14b

Please sign in to comment.