diff --git a/README.md b/README.md index 0a5e022..51f3d0e 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,14 @@ It will display in React-devtools. Defaults to `UserAuthWrapper` If it evaluates to false the browser will be redirected to `failureRedirectPath`, otherwise `DecoratedComponent` will be rendered. * `[allowRedirect]` \(*Bool*): Optional bool on whether to pass a `redirect` query parameter to the `failureRedirectPath` +#### Returns +After applying the configObject, `UserAuthWrapper` returns a function which can applied to a Component to wrap in authentication and +authorization checks. The function also has the following extra properties: +* `onEnter(store, nextState, replace)` \(*Function*): Function to be optionally used in the [onEnter](https://github.com/reactjs/react-router/blob/master/docs/API.md#onenternextstate-replace-callback) property of a route. + #### Component Parameter -* `DecoratedComponent` \(*React Component*): The component to be wrapped in the auth check. It will pass down all props given to the returned component as well as the prop `authData` which is the result of the `authSelector` +* `DecoratedComponent` \(*React Component*): The component to be wrapped in the auth check. It will pass down all props given to the returned component as well as the prop `authData` which is the result of the `authSelector`. +The component is not modified and all static properties are hoisted to the returned component ## Authorization & More Advanced Usage @@ -171,6 +177,32 @@ class MyComponents extends Component { } ``` +### Server Side Rendering +In order to perform authentication and authorization checks for Server Side Rendering, you may need to use the `onEnter` property +of a ``. You can access the `onEnter` method of the UserAuthWrapper after applying the config parameters: +```js +import { UserAuthWrapper } from 'redux-auth-wrapper'; + +const UserIsAuthenticated = UserAuthWrapper({ + authSelector: state => state.user, + redirectAction: routeActions.replace, + wrapperDisplayName: 'UserIsAuthenticated' +}) + +const getRoutes = (store) => { + const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState); + + return ( + + + + + + + ); +}; +``` + ### Other examples * [Redux-Router and React-Router 1.0 with JWT](https://github.com/mjrussell/react-redux-jwt-auth-example/tree/auth-wrapper) * [React-Router-Redux and React-Router 2.0 with JWT](https://github.com/mjrussell/react-redux-jwt-auth-example/tree/react-router-redux) diff --git a/package.json b/package.json index ec4f286..b6304f3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "react": "~0.14.3", "react-addons-test-utils": "^0.14.6", "react-redux": "^4.0.1", - "react-router": "2.0.0-rc5", + "react-router": "2.0.0", "react-router-redux": "~3.0.0", "redux": "^3.2.0" }, diff --git a/src/index.js b/src/index.js index 56d86f1..2a6cd9c 100644 --- a/src/index.js +++ b/src/index.js @@ -16,8 +16,27 @@ const UserAuthWrapper = (args) => { ...defaults, ...args } + + const isAuthorized = (authData) => predicate(authData) + + const ensureAuth = ({ location, authData }, redirect) => { + let query + if (allowRedirectBack) { + query = { redirect: `${location.pathname}${location.search}` } + } else { + query = {} + } + + if (!isAuthorized(authData)) { + redirect({ + pathname: failureRedirectPath, + query + }) + } + } + // Wraps the component that needs the auth enforcement - return function wrapComponent(DecoratedComponent) { + function wrapComponent(DecoratedComponent) { const displayName = DecoratedComponent.displayName || DecoratedComponent.name || 'Component' const mapDispatchToProps = (dispatch) => { @@ -51,16 +70,16 @@ const UserAuthWrapper = (args) => { }; componentWillMount() { - this.ensureAuth(this.props) + ensureAuth(this.props, this.getRedirectFunc(this.props)) } componentWillReceiveProps(nextProps) { - this.ensureAuth(nextProps) + ensureAuth(nextProps, this.getRedirectFunc(nextProps)) } - getRedirectFunc = () => { - if (this.props.redirect) { - return this.props.redirect + getRedirectFunc = (props) => { + if (props.redirect) { + return props.redirect } else { if (!this.context.router.replace) { /* istanbul ignore next sanity */ @@ -71,31 +90,12 @@ const UserAuthWrapper = (args) => { } }; - isAuthorized = (authData) => predicate(authData); - - ensureAuth = (props) => { - const { location, authData } = props - let query - if (allowRedirectBack) { - query = { redirect: `${location.pathname}${location.search}` } - } else { - query = {} - } - - if (!this.isAuthorized(authData)) { - this.getRedirectFunc()({ - pathname: failureRedirectPath, - query - }) - } - }; - render() { // Allow everything but the replace aciton creator to be passed down // Includes route props from React-Router and authData const { redirect, authData, ...otherProps } = this.props - if (this.isAuthorized(authData)) { + if (isAuthorized(authData)) { return } else { // Don't need to display anything because the user will be redirected @@ -106,6 +106,13 @@ const UserAuthWrapper = (args) => { return hoistStatics(UserAuthWrapper, DecoratedComponent) } + + wrapComponent.onEnter = (store, nextState, replace) => { + const authData = authSelector(store.getState()) + ensureAuth({ location: nextState.location, authData }, replace) + } + + return wrapComponent } // Support the old 0.1.x with deprecation warning diff --git a/test/UserAuthWrapper-test.js b/test/UserAuthWrapper-test.js index fe61109..a64f550 100644 --- a/test/UserAuthWrapper-test.js +++ b/test/UserAuthWrapper-test.js @@ -110,7 +110,7 @@ class UnprotectedParentComponent extends Component { } } -const routes = ( +const defaultRoutes = ( @@ -135,7 +135,7 @@ const userLoggedIn = (firstName = 'Test', lastName = 'McDuderson') => { } } -const setupTest = () => { +const setupTest = (routes = defaultRoutes) => { const history = createMemoryHistory() const store = configureStore(history) @@ -299,4 +299,25 @@ describe('UserAuthWrapper', () => { expect(authed.staticFun).to.be.a('function') expect(authed.staticFun()).to.equal('auth') }) + + it('provides an onEnter static function', () => { + let store + const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) + + const routesOnEnter = ( + + + + + ) + + const { history, store: createdStore } = setupTest(routesOnEnter) + store = createdStore + + expect(store.getState().routing.location.pathname).to.equal('/') + expect(store.getState().routing.location.search).to.equal('') + history.push('/onEnter') + expect(store.getState().routing.location.pathname).to.equal('/login') + expect(store.getState().routing.location.search).to.equal('?redirect=%2FonEnter') + }) })