Skip to content

Commit

Permalink
Merge pull request #57 from mjrussell/failure-redir-func
Browse files Browse the repository at this point in the history
Failure Redirect Function Option
  • Loading branch information
mjrussell authored Jul 23, 2016
2 parents ef3b771 + 8f785c6 commit 03fe782
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 14 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,14 @@ Any time the user data changes, the UserAuthWrapper will re-check for authentica
ownProps will be null if isOnEnter is true because onEnter hooks cannot receive the component properties. Can be ignored when not using onEnter.
* `authenticatingSelector(state, [ownProps]): Bool` \(*Function*): A state selector indicating if the user is currently authenticating. Just like `mapToStateProps`. Useful for async session loading.
* `LoadingComponent` \(*Component*): A React component to render while `authenticatingSelector` is `true`. If not present, will be a `<span/>`.
* `[failureRedirectPath]` \(*String*): Optional path to redirect the browser to on a failed check. Defaults to `/login`
* `[failureRedirectPath]` \(*String | (state, [ownProps]): String*): Optional path to redirect the browser to on a failed check. Defaults to `/login`. Can also be a function of state and ownProps that returns a string.
* `[redirectQueryParamName]` \(*String*): Optional name of the query parameter added when `allowRedirectBack` is true. Defaults to `redirect`.
* `[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 `authSelector` param.
If it evaluates to false the browser will be redirected to `failureRedirectPath`, otherwise `DecoratedComponent` will be rendered.
* `[allowRedirectBack]` \(*Bool*): Optional bool on whether to pass a `redirect` query parameter to the `failureRedirectPath`
If it evaluates to false the browser will be redirected to `failureRedirectPath`, otherwise `DecoratedComponent` will be rendered. By default, it returns true if `authData` is {} or null.
* `[allowRedirectBack]` \(*Bool*): Optional bool on whether to pass a `redirect` query parameter to the `failureRedirectPath`. Defaults to `true`.

#### Returns
After applying the configObject, `UserAuthWrapper` returns a function which can applied to a Component to wrap in authentication and
Expand Down
26 changes: 16 additions & 10 deletions src/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import isEmpty from 'lodash.isempty'
const defaults = {
LoadingComponent: 'span',
failureRedirectPath: '/login',
redirectQueryParamName: 'redirect',
wrapperDisplayName: 'AuthWrapper',
predicate: x => !isEmpty(x),
authenticatingSelector: () => false,
Expand All @@ -16,23 +17,24 @@ export default function factory(React, empty) {
const { Component, PropTypes } = React

return (args) => {
const { authSelector, authenticatingSelector, LoadingComponent, failureRedirectPath, wrapperDisplayName, predicate, allowRedirectBack, redirectAction } = {
...defaults,
...args
}
const { authSelector, authenticatingSelector, LoadingComponent, failureRedirectPath,
wrapperDisplayName, predicate, allowRedirectBack, redirectAction, redirectQueryParamName } = {
...defaults,
...args
}

const isAuthorized = (authData) => predicate(authData)

const createRedirect = (location, redirect) => {
const createRedirect = (location, redirect, redirectPath) => {
let query
if (allowRedirectBack) {
query = { redirect: `${location.pathname}${location.search}` }
query = { [redirectQueryParamName]: `${location.pathname}${location.search}` }
} else {
query = {}
}

redirect({
pathname: failureRedirectPath,
pathname: redirectPath,
query
})
}
Expand All @@ -53,6 +55,7 @@ export default function factory(React, empty) {
(state, ownProps) => {
return {
authData: authSelector(state, ownProps, false),
failureRedirectPath: typeof failureRedirectPath === 'function' ? failureRedirectPath(state, ownProps) : failureRedirectPath,
isAuthenticating: authenticatingSelector(state, ownProps)
}
},
Expand All @@ -63,6 +66,7 @@ export default function factory(React, empty) {
static displayName = `${wrapperDisplayName}(${displayName})`;

static propTypes = {
failureRedirectPath: PropTypes.string.isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
search: PropTypes.string.isRequired
Expand All @@ -78,7 +82,7 @@ export default function factory(React, empty) {

componentWillMount() {
if(!this.props.isAuthenticating && !isAuthorized(this.props.authData)) {
createRedirect(this.props.location, this.getRedirectFunc(this.props))
createRedirect(this.props.location, this.getRedirectFunc(this.props), this.props.failureRedirectPath)
}
}

Expand All @@ -94,7 +98,7 @@ export default function factory(React, empty) {
// 2. Was not authorized and authenticating but no longer authenticating
(wasAuthenticating && !willbeAuthenticating && !willBeAuthorized)
) {
createRedirect(nextProps.location, this.getRedirectFunc(nextProps))
createRedirect(nextProps.location, this.getRedirectFunc(nextProps), nextProps.failureRedirectPath)
}
}

Expand Down Expand Up @@ -131,8 +135,10 @@ export default function factory(React, empty) {

wrapComponent.onEnter = (store, nextState, replace) => {
const authData = authSelector(store.getState(), null, true)
const redirectPath = typeof failureRedirectPath === 'function' ? failureRedirectPath(store.getState(), null) : failureRedirectPath

if (!isAuthorized(authData)) {
createRedirect(nextState.location, replace)
createRedirect(nextState.location, replace, redirectPath)
}
}

Expand Down
63 changes: 62 additions & 1 deletion test/UserAuthWrapper-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,67 @@ describe('UserAuthWrapper', () => {
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('?redirect=%2FownProps%2F2')
})

it('can override query param name', () => {
const UserIsAuthenticatedQueryParam = UserAuthWrapper({
authSelector: userSelector,
redirectQueryParamName: 'customRedirect',
redirectAction: routerActions.replace
})

const routes = (
<Route path="/" component={App} >
<Route path="login" component={UnprotectedComponent} />
<Route path="protected" component={UserIsAuthenticatedQueryParam(UnprotectedComponent)} />
</Route>
)

const { history, store } = setupTest(routes)

expect(store.getState().routing.locationBeforeTransitions.pathname).to.equal('/')
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('')

history.push('/protected')
expect(store.getState().routing.locationBeforeTransitions.pathname).to.equal('/login')
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('?customRedirect=%2Fprotected')
})

it('can pass a selector for failureRedirectPath', () => {
const failureRedirectFn = (state, ownProps) => {
if (userSelector(state) === undefined && ownProps.routeParams.id === '1') {
return '/login/1'
} else {
return '/login/0'
}
}

const UserIsAuthenticatedProps = UserAuthWrapper({
authSelector: userSelector,
failureRedirectPath: failureRedirectFn,
redirectAction: routerActions.replace,
wrapperDisplayName: 'UserIsAuthenticatedProps'
})

const routes = (
<Route path="/" component={App} >
<Route path="login/:id" component={UnprotectedComponent} />
<Route path="ownProps/:id" component={UserIsAuthenticatedProps(UnprotectedComponent)} />
</Route>
)

const { history, store } = setupTest(routes)

expect(store.getState().routing.locationBeforeTransitions.pathname).to.equal('/')
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('')

history.push('/ownProps/1')
expect(store.getState().routing.locationBeforeTransitions.pathname).to.equal('/login/1')
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('?redirect=%2FownProps%2F1')

history.push('/ownProps/2')
expect(store.getState().routing.locationBeforeTransitions.pathname).to.equal('/login/0')
expect(store.getState().routing.locationBeforeTransitions.search).to.equal('?redirect=%2FownProps%2F2')
})

it('uses router for redirect if no redirectAction specified', () => {

const UserIsAuthenticatedNoAction = UserAuthWrapper({
Expand Down Expand Up @@ -468,7 +529,7 @@ describe('UserAuthWrapper', () => {
const store = createStore(rootReducer)
mount(
<Provider store={store}>
<Component location={{ pathname: '/', query: {} }}/>
<Component location={{ pathname: '/', query: {}, search: '' }}/>
</Provider>
)

Expand Down

0 comments on commit 03fe782

Please sign in to comment.