├── .babelrc ├── .bookignore ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── README.md ├── SUMMARY.md ├── _layouts └── website │ └── page.html ├── book.json ├── docs ├── API.md ├── AdvancedUsage │ ├── OtherRouters.md │ ├── ReactRouter3.md │ └── Redux.md ├── Getting-Started │ ├── HidingAlternate.md │ ├── NestingWrappers.md │ ├── Overview.md │ ├── ReactNative.md │ ├── ReactRouter3.md │ └── ReactRouter4.md ├── Migrating.md └── Troubleshooting.md ├── examples ├── react-router-3 │ ├── .babelrc │ ├── README.md │ ├── actions │ │ └── user.js │ ├── app.js │ ├── auth.js │ ├── components │ │ ├── Admin.js │ │ ├── App.js │ │ ├── Foo.js │ │ ├── Home.js │ │ ├── Loading.js │ │ ├── Login.js │ │ └── index.js │ ├── constants.js │ ├── index.html │ ├── package.json │ ├── reducers │ │ ├── index.js │ │ └── user.js │ ├── webpack.config.babel.js │ └── yarn.lock └── react-router-4 │ ├── .babelrc │ ├── README.md │ ├── actions │ └── user.js │ ├── app.js │ ├── auth.js │ ├── components │ ├── Admin.js │ ├── App.css │ ├── App.js │ ├── Home.js │ ├── Loading.js │ ├── Login.js │ └── Protected.js │ ├── constants.js │ ├── index.html │ ├── package.json │ ├── reducers │ ├── index.js │ └── user.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── package.json ├── runTests.sh ├── src ├── authWrapper.js ├── connectedAuthWrapper.js ├── helper │ └── redirect.js ├── history3 │ ├── locationHelper.js │ └── redirect.js ├── history4 │ ├── locationHelper.js │ └── redirect.js ├── index.js └── redirect.js ├── test ├── authWrapper-test.js ├── helpers.js ├── init.js ├── redirectBase-test.js ├── rrv3-test.js └── rrv4-test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.bookignore: -------------------------------------------------------------------------------- 1 | src/ 2 | lib/ 3 | coverage 4 | test/ 5 | examples/ 6 | package.json 7 | .babelrc 8 | .eslintrc 9 | .gitignore 10 | .travis.yml 11 | CHANGELOG.md 12 | LICENSE.txt 13 | runTests.sh 14 | .node-version 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "react": { 4 | "version": "detect" 5 | } 6 | }, 7 | "parser": "babel-eslint", 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended" 11 | ], 12 | "rules": { 13 | "react/prop-types": 0, 14 | "react/display-name": 0, 15 | "react/no-string-refs": 0 16 | }, 17 | "env": { 18 | "browser": true, 19 | "node": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | examples/**/bundle.js 3 | node_modules 4 | coverage 5 | npm-debug.log 6 | .idea/ 7 | _book 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | node_js: 7 | - "8" 8 | - "10" 9 | - "12" 10 | env: 11 | - REACT_ROUTER_VERSION=3 12 | - REACT_ROUTER_VERSION=4 13 | script: 14 | - yarn run lint 15 | - ./runTests.sh 16 | after_success: 17 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [HEAD](https://github.com/mjrussell/redux-auth-wrapper/compare/v3.0.0...HEAD) 2 | -Nothing yet 3 | 4 | ## [3.0.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v2.1.0...v3.0.0) 5 | - **Breaking Change:** Fix deprecated lifecyle method for react 16.3+ support[#250](https://github.com/mjrussell/redux-auth-wrapper/pull/250) (contributed by @JackHowa) 6 | - Dependency upgrades for react 16.3 support 7 | 8 | ## [2.1.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v2.0.3...v2.1.0) 9 | - Upgrade deps and relax bounds [#244](https://github.com/mjrussell/redux-auth-wrapper/pull/244) 10 | 11 | ## [2.0.3](https://github.com/mjrussell/redux-auth-wrapper/compare/v2.0.2...v2.0.3) 12 | - Warning fix for React 16.3+ [#235](https://github.com/mjrussell/redux-auth-wrapper/pull/235) (contributed by @tpai) 13 | 14 | ## [2.0.2](https://github.com/mjrussell/redux-auth-wrapper/compare/v2.0.1...v2.0.2) 15 | - **Bugfix:** Prevents unnecessary re-renders on child subcomponents on state changes [#187](https://github.com/mjrussell/redux-auth-wrapper/issues/187) 16 | 17 | ## [2.0.1](https://github.com/mjrussell/redux-auth-wrapper/compare/v2.0.0...v2.0.1) 18 | - **Bugfix:** Adds empty main with index.js for webpack ddl support (Contributed by @victorouse) 19 | 20 | ## [2.0.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v1.1.0...v2.0.0) 21 | **BREAKING CHANGES** Version 2.0 include several new changes! See https://mjrussell.github.io/redux-auth-wrapper/docs/Migrating.html for migrating from 1.x 22 | 23 | ## [1.1.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v1.0.0...v1.1.0) 24 | - **Feature:** Support hash fragments in building the redirect location [#121](https://github.com/mjrussell/redux-auth-wrapper/issues/121) 25 | - **Feature:** Upgrade to proptypes package after React 15.5 deprecation [#147](https://github.com/mjrussell/redux-auth-wrapper/pull/147) (Contributed by @jruhland) 26 | 27 | ## [1.0.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.10.0...v1.0.0) 28 | - **Bugfix:** Redirection preserves query params in the redirect path. [#111](https://github.com/mjrussell/redux-auth-wrapper/pull/111) 29 | 30 | ## [0.10.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.9.0...v0.10.0) 31 | - **Feature:** `allowRedirectBack` can also take a selector function that returns a bool instead of a bool. [#93](https://github.com/mjrussell/redux-auth-wrapper/pull/93) (Contributed by @oyeanuj) 32 | 33 | ## [0.9.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.8.0...v0.9.0) 34 | - **Bugfix:** Don't pass down auth wrapper props besides authData [#81](https://github.com/mjrussell/redux-auth-wrapper/issues/81) 35 | - **Feature:** Add propMapper function to restrict passed through props [#28](https://github.com/mjrussell/redux-auth-wrapper/issues/28) 36 | - **Bugfix/Breaking Change:** onEnter now checks isAuthenticating for redirection [#89](https://github.com/mjrussell/redux-auth-wrapper/issues/89) 37 | - **Feature:** onEnter selectors receive nextState as the second argument instead of null [#90](https://github.com/mjrussell/redux-auth-wrapper/issues/90) 38 | 39 | ## [0.8.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.7.0...v0.8.0) 40 | - **Feature:** FailureComponent to not redirect and either hide or show a different component on auth failure [#61](https://github.com/mjrussell/redux-auth-wrapper/pull/61). (Contributed by @mehiel) 41 | - **Feature:** Pass auth data as props to Loading Component [#75](https://github.com/mjrussell/redux-auth-wrapper/issues/75) 42 | - **Breaking Change:** When rendering an "empty" component such as the default LoadingComponent and when the user is about 43 | to be redirect, return `null` so React will omit it instead of an empty `div`. 44 | - **Breaking Change:** Don't need to import React Native compatible from lib. 45 | 46 | ## [0.7.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.6.0...v0.7.0) 47 | - **Bugfix:** Don't render subroutes as children in default LoadingComponent [#63](https://github.com/mjrussell/redux-auth-wrapper/pull/63) 48 | - **Bugfix/Breaking Change:** Change default LoadingComponent to a `div` and use the React native default empty element [#63](https://github.com/mjrussell/redux-auth-wrapper/pull/63) 49 | 50 | ## [0.6.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.5.2...v0.6.0) 51 | - **Feature:** `failureRedirectPath` can be a function of state and props [#57](https://github.com/mjrussell/redux-auth-wrapper/pull/57) 52 | - **Feature:** option to change the query param name with `redirectQueryParamName` [#57](https://github.com/mjrussell/redux-auth-wrapper/pull/57) 53 | 54 | ## [0.5.2](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.5.1...v0.5.2) 55 | - **BugFix:** Fixes bug introduced in v0.5.1 that prevented redirection when only isAuthenticating changed [#49](https://github.com/mjrussell/redux-auth-wrapper/issues/49) 56 | 57 | ## [0.5.1](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.5.0...v0.5.1) 58 | - **BugFix:** Adds safeguard to prevent infinite redirects from the wrapper [#45](https://github.com/mjrussell/redux-auth-wrapper/pull/45) 59 | 60 | ## [0.5](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.4.0...v0.5.0) 61 | - **Feature:** Adds `isAuthenticating` selector and `LoadingComponent` 62 | [#35](https://github.com/mjrussell/redux-auth-wrapper/pull/35). (Contributed by @cab) 63 | 64 | ## [0.4](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.3.0...v0.4.0) 65 | - **Feature:** Adds React Native support [#33](https://github.com/mjrussell/redux-auth-wrapper/pull/33) 66 | 67 | ## [0.3](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.2.1...v0.3.0) 68 | - **Feature:** Adds `ownProps` param to `authSelector` [#21](https://github.com/mjrussell/redux-auth-wrapper/pull/21) 69 | - **Feature:** Adds `onEnter` function for Server Side Rendering support [#19](https://github.com/mjrussell/redux-auth-wrapper/pull/19) 70 | - **Breaking:** Removes arg style syntax that was deprecated in 0.2 71 | 72 | ## [0.2.1](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.2.0...v0.2.1) 73 | - router context is only required if no redirectAction 74 | 75 | ## [0.2.0](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.1.1...v0.2.0) 76 | - **Feature:** new redirectAction config arg, removes dependency on a redux-routing implementation [#13](https://github.com/mjrussell/redux-auth-wrapper/issues/13) 77 | - **Feature:** New config object syntax for AuthWrapper [#12](https://github.com/mjrussell/redux-auth-wrapper/issues/12) 78 | - **Deprecation:** Deprecates AuthWrapper args syntax [#12](https://github.com/mjrussell/redux-auth-wrapper/issues/12) 79 | - **Feature:** Hoists wrapped component's statics up to the returned component 80 | 81 | ## [0.1.1](https://github.com/mjrussell/redux-auth-wrapper/compare/v0.1.0...v0.1.1) 82 | - Fixes the bad npm publish 83 | 84 | ## [0.1.0](https://github.com/mjrussell/redux-auth-wrapper/compare/fcbf49d0abcae7075daa146c05edff1b735b3a16...v0.1.0) 85 | - First release! 86 | - Adds AuthWrapper with args syntax 87 | - Examples using Redux-Simple-Router (now React-Router-Redux) 88 | - Lots of tests 89 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mjrussell@users.noreply.github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Matthew Russell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-auth-wrapper 2 | 3 | [![npm](https://img.shields.io/npm/v/redux-auth-wrapper.svg)](https://www.npmjs.com/package/redux-auth-wrapper) 4 | [![npm dm](https://img.shields.io/npm/dm/redux-auth-wrapper.svg)](https://www.npmjs.com/package/redux-auth-wrapper) 5 | [![Build Status](https://travis-ci.org/mjrussell/redux-auth-wrapper.svg?branch=master)](https://travis-ci.org/mjrussell/redux-auth-wrapper) 6 | [![Coverage Status](https://coveralls.io/repos/github/mjrussell/redux-auth-wrapper/badge.svg?branch=master)](https://coveralls.io/github/mjrussell/redux-auth-wrapper?branch=master) 7 | [![Join the chat at https://gitter.im/mjrussell/redux-auth-wrapper](https://badges.gitter.im/mjrussell/redux-auth-wrapper.svg)](https://gitter.im/mjrussell/redux-auth-wrapper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | **Decouple your Authentication and Authorization from your components!** 10 | 11 | `npm install --save redux-auth-wrapper` 12 | 13 | redux-auth-wrapper is a utility library for handling authentication and authorization in react + redux applications. 14 | 15 | Read the documentation at https://mjrussell.github.io/redux-auth-wrapper 16 | 17 | ## Version 3 18 | Version 3.x has the same external API as version 2, however it only supports React >= 16.3. It is also tested with react-router v5 and [connected-react-router](https://github.com/supasate/connected-react-router) which replaced [react-router-redux](https://github.com/reactjs/react-router-redux). 19 | 20 | ## Version 2 21 | 22 | Version 2.x is a big internal rewrite! It provides a massive increase in flexibility when using redux-auth-wrapper and also introduces some breaking changes. See the [Migration Guide](https://mjrussell.github.io/redux-auth-wrapper/docs/Migrating.html) for more details if coming from 1.x. Or check out the [Getting Started](https://mjrussell.github.io/redux-auth-wrapper/docs/Getting-Started/Overview.html) guide if you've never used redux-auth-wrapper before. 23 | 24 | Looking for Version 1.x? You can browse the 1.x README [here](https://github.com/mjrussell/redux-auth-wrapper/tree/1.x). 25 | 26 | ## Submitting Issues 27 | 28 | Having trouble? First check out the [Troubleshooting](https://mjrussell.github.io/redux-auth-wrapper/docs/Troubleshooting.html) section of the documentation, and then search the issues, both open and closed for your problem. If you are still having trouble or have a question on using redux-auth-wrapper, please open an issue! You can also ask on the gitter channel. 29 | 30 | ## Examples 31 | * [React Router 3](https://github.com/mjrussell/redux-auth-wrapper/tree/master/examples/react-router-3) 32 | * [React Router 4/5](https://github.com/mjrussell/redux-auth-wrapper/tree/master/examples/react-router-4) 33 | 34 | Other examples not yet updated to v2: 35 | * [Redux-Router and React-Router 1.0 with JWT](https://github.com/mjrussell/react-redux-jwt-auth-example/tree/auth-wrapper) 36 | * [React-Router-Redux and React-Router 2.0 with JWT](https://github.com/mjrussell/react-redux-jwt-auth-example/tree/react-router-redux) 37 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | * [Read Me](README.md) 4 | * [Getting Started](docs/Getting-Started/Overview.md) 5 | * [React Router 3](docs/Getting-Started/ReactRouter3.md) 6 | * [React Router 4](docs/Getting-Started/ReactRouter4.md) 7 | * [React Native](docs/Getting-Started/ReactNative.md) 8 | * [Hiding and Alternate Components](docs/Getting-Started/HidingAlternate.md) 9 | * [Nesting Wrappers](docs/Getting-Started/NestingWrappers.md) 10 | * [Migrating from V1](docs/Migrating.md) 11 | * [Troubleshooting](docs/Troubleshooting.md) 12 | * [API](docs/API.md) 13 | * Advanced Usage 14 | * [Integrating with other Routers](docs/AdvancedUsage/OtherRouters.md) 15 | * [React Router3 and SSR](docs/AdvancedUsage/ReactRouter3.md) 16 | * [Advanced Redux](docs/AdvancedUsage/Redux.md) 17 | -------------------------------------------------------------------------------- /_layouts/website/page.html: -------------------------------------------------------------------------------- 1 | {% extends template.self %} 2 | 3 | {% block head %} 4 | {{ super() }} 5 | 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.2", 3 | "plugins": ["prism@1.0.0", "-highlight", "github", "github-edit", "heading-anchors", "ga@1.0.1"], 4 | "pluginsConfig": { 5 | "github": { 6 | "url": "https://github.com/mjrussell/redux-auth-wrapper" 7 | }, 8 | "ga": { 9 | "token": "UA-102064405-1" 10 | }, 11 | "github-edit": { 12 | "repo": "mjrussell/redux-auth-wrapper", 13 | "branch": "master" 14 | }, 15 | "sharing": { 16 | "facebook": false, 17 | "twitter": true, 18 | "google": false, 19 | "weibo": false, 20 | "instapaper": false, 21 | "vk": false 22 | }, 23 | "theme-default": { 24 | "styles": { } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | #### How to read the API 4 | 5 | * Parameter names beginning with `?` are optional 6 | * `HigherOrderComponent` is a function of type: 7 | ``` 8 | ReactClass | ReactFunctionalComponent | string => 9 | ReactClass | ReactFunctionalComponent | string 10 | ``` 11 | 12 | ## Redirection Helpers (React Router 3/History v3) 13 | 14 | ### `connectedRouterRedirect` 15 | 16 | ```js 17 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history3/redirect' 18 | 19 | connectedRouterRedirect({ 20 | redirectPath: string | (state: Object, ownProps: Object) => string, 21 | authenticatedSelector: (state: Object, ownProps: Object) => boolean, 22 | ?authenticatingSelector: (state: Object, ownProps: Object) => boolean, 23 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 24 | ?wrapperDisplayName: string, 25 | ?allowRedirectBack: boolean | (nextState: Object, redirectPath: String) => boolean, 26 | ?redirectQueryParamName: string 27 | }): HigherOrderComponent 28 | ``` 29 | 30 | ### `connectedReduxRedirect` 31 | 32 | ```js 33 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history3/redirect' 34 | 35 | connectedReduxRedirect({ 36 | redirectPath: string | (state: Object, ownProps: Object) => string, 37 | authenticatedSelector: (state: Object, ownProps: Object) => boolean, 38 | ?authenticatingSelector: (state: Object, ownProps: Object) => boolean, 39 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 40 | ?wrapperDisplayName: string, 41 | ?allowRedirectBack: boolean | (nextState: Object, redirectPath: String) => boolean, 42 | ?redirectQueryParamName: string 43 | }): HigherOrderComponent 44 | ``` 45 | 46 | ### `createOnEnter` 47 | 48 | ```js 49 | import { createOnEnter } from 'redux-auth-wrapper/history3/redirect' 50 | 51 | createOnEnter({ 52 | redirectPath: string | (state: Object, nextState: Object) => string, 53 | authenticatedSelector: (state: Object, nextState: Object) => boolean, 54 | ?authenticatingSelector: (state: Object, nextState: Object) => boolean, 55 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 56 | ?wrapperDisplayName: string, 57 | ?allowRedirectBack: boolean | (nextState: Object, redirectPath: String) => boolean, 58 | ?redirectQueryParamName: string 59 | }): (store: Object, nextState: Object: replace: (location: Object => void)) 60 | ``` 61 | 62 | ### `locationHelperBuilder` 63 | 64 | Helper used by the redirection and useful for pulling the redirectPath out of the query params. 65 | 66 | ```js 67 | import locationHelperBuilder from 'redux-auth-wrapper/history3/locationHelper' 68 | 69 | locationHelperBuilder({ 70 | ?redirectQueryParamName: string, 71 | ?locationSelector: (props: Object) => LocationObject 72 | }) : LocationHelper 73 | 74 | 75 | LocationHelper: { 76 | getRedirectQueryParam: (props: Object) => string, 77 | createRedirectLoc: allowRedirectBack: boolean => (nextState: Object, redirectPath: string) => LocationObject, 78 | } 79 | ``` 80 | 81 | ## Redirection Helpers (React Router 4/History v4) 82 | 83 | ### `connectedRouterRedirect` 84 | 85 | ```js 86 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history4/redirect' 87 | 88 | connectedRouterRedirect({ 89 | redirectPath: string | (state: Object, ownProps: Object) => string, 90 | authenticatedSelector: (state: Object, ownProps: Object) => boolean, 91 | ?authenticatingSelector: (state: Object, ownProps: Object) => boolean, 92 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 93 | ?wrapperDisplayName: string, 94 | ?allowRedirectBack: boolean | (nextState: Object, redirectPath: String) => boolean, 95 | ?redirectQueryParamName: string 96 | }): HigherOrderComponent 97 | ``` 98 | 99 | ### `connectedReduxRedirect` 100 | 101 | ```js 102 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history4/redirect' 103 | 104 | connectedReduxRedirect({ 105 | redirectPath: string | (state: Object, ownProps: Object) => string, 106 | redirectAction: (location: Object) => ReduxAction, 107 | authenticatedSelector: (state: Object, ownProps: Object) => boolean, 108 | ?authenticatingSelector: (state: Object, ownProps: Object) => boolean, 109 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 110 | ?wrapperDisplayName: string, 111 | ?allowRedirectBack: boolean | (nextState: Object, redirectPath: String) => boolean, 112 | ?redirectQueryParamName: string 113 | }): HigherOrderComponent 114 | ``` 115 | 116 | ### `locationHelperBuilder` 117 | 118 | ```js 119 | import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper' 120 | 121 | locationHelperBuilder({ 122 | ?redirectQueryParamName: string, 123 | ?locationSelector: (props: Object) => LocationObject 124 | }) : LocationHelper 125 | 126 | 127 | LocationHelper: { 128 | getRedirectQueryParam: (props: Object) => string, 129 | createRedirectLoc: allowRedirectBack: boolean => (nextState: Object, redirectPath: string) => LocationObject, 130 | } 131 | ``` 132 | 133 | ## Other Wrappers 134 | 135 | ### `authWrapper` 136 | 137 | ```js 138 | import authWrapper from 'redux-auth-wrapper/authWrapper' 139 | 140 | authWrapper({ 141 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 142 | ?FailureComponent: ReactClass | ReactFunctionalComponent | string, 143 | ?wrapperDisplayName: string 144 | }): HigherOrderComponent 145 | ``` 146 | 147 | The returned Component after applying a Component to the HOC takes as props `isAuthenticated` and `isAuthenticating`, both of which are booleans. `isAuthenticating` defaults to `false`. 148 | 149 | ### `connectedAuthWrapper` 150 | 151 | ```js 152 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 153 | 154 | connectedAuthWrapper({ 155 | authenticatedSelector: (state: Object, ownProps: Object) => boolean, 156 | ?authenticatingSelector: (state: Object, ownProps: Object) => boolean, 157 | ?AuthenticatingComponent: ReactClass | ReactFunctionalComponent | string, 158 | ?FailureComponent: ReactClass | ReactFunctionalComponent | string, 159 | ?wrapperDisplayName: string 160 | }): HigherOrderComponent 161 | ``` 162 | 163 | ## Other Helpers 164 | 165 | Documentation in progress! 166 | -------------------------------------------------------------------------------- /docs/AdvancedUsage/OtherRouters.md: -------------------------------------------------------------------------------- 1 | # Integrating with other Routers 2 | 3 | Help us improve this documentation by submitting a pull request! 4 | -------------------------------------------------------------------------------- /docs/AdvancedUsage/ReactRouter3.md: -------------------------------------------------------------------------------- 1 | # React Router 3 Advanced Usage 2 | 3 | ## Protecting Multiple Routes 4 | 5 | Because routes in React Router 3 are not required to have paths, you can use nesting to protect multiple routes without applying 6 | the wrapper multiple times. 7 | ```js 8 | const Authenticated = userIsAuthenticated(({ children, ...props }) => React.cloneElement(children, props)); 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | One thing to note is that if you use named children routes (TODO example) then you may need to use a different approach than `cloneElement` with the `children` prop. 21 | 22 | ## Server Side Rendering 23 | 24 | If you are using redux-auth-wrapper for redirection in apps that use Server Side Rendering you may need to use the `onEnter`. This is because in most cases your server should redirect users before sending down the client HTML. 25 | 26 | ```js 27 | import { createOnEnter } from '../src/history3/redirect' 28 | 29 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) 30 | 31 | const userIsAuthenticated = connectedReduxRedirect({ 32 | redirectPath: 'unprotected', 33 | authenticatedSelector: state => state.user !== null, 34 | }) 35 | 36 | const onEnter = createOnEnter({ 37 | redirectPath: 'unprotected', 38 | authenticatedSelector: state => state.user !== null, 39 | }) 40 | 41 | 42 | 43 | 45 | ``` 46 | 47 | During onEnter, selectors such as `authenticatedSelector`, `authenticatingSelector`, and `failureRedirectPath` (if you are using) 48 | the function variation, will receive react-router's `nextState` as their second argument instead of the component props. 49 | 50 | ### With nested wrappers 51 | 52 | To implement SSR with nested wrappers, you will have to provide a function to chain `onEnter` functions of each wrapper. 53 | 54 | ```js 55 | const onEnterAuth = createOnEnter({ 56 | redirectPath: 'unprotected', 57 | authenticatedSelector: state => state.user !== null, 58 | }) 59 | 60 | const userIsAuthenticated = connectedReduxRedirect({ 61 | redirectPath: 'unprotected', 62 | authenticatedSelector: state => state.user !== null, 63 | }) 64 | 65 | const onEnterAdmin = createOnEnter({ 66 | redirectPath: 'home', 67 | authenticatedSelector: state => state.user.isAdmin === false, 68 | }) 69 | 70 | const userIsAdmin = connectedReduxRedirect({ 71 | redirectPath: 'home', 72 | authenticatedSelector: state => state.user.isAdmin === false, 73 | }) 74 | 75 | const getRoutes = (store) => { 76 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState); 77 | 78 | //This executes the parent onEnter first, going from left to right. 79 | // `replace` has to be wrapped because we want to stop executing `onEnter` hooks 80 | // after the first call to `replace`. 81 | const onEnterChain = (...listOfOnEnters) => (store, nextState, replace) => { 82 | let redirected = false; 83 | const wrappedReplace = (...args) => { 84 | replace(...args); 85 | redirected = true; 86 | }; 87 | listOfOnEnters.forEach((onEnter) => { 88 | if (!redirected) { 89 | onEnter(store, nextState, wrappedReplace); 90 | } 91 | }); 92 | }; 93 | 94 | return ( 95 | 96 | 97 | 98 | 101 | 104 | 105 | 106 | ); 107 | }; 108 | 109 | ``` 110 | -------------------------------------------------------------------------------- /docs/AdvancedUsage/Redux.md: -------------------------------------------------------------------------------- 1 | # Redux Advanced Usage 2 | 3 | ## Dispatching an Additional Redux Action on Redirect 4 | You may want to dispatch an additional redux action when a redirect occurs. One example of this is to display a notification message 5 | that the user is being redirected or doesn't have access to that protected resource. To do this, you can chain the `redirectAction` 6 | parameter using `redux-thunk` middleware. It depends slightly on if you are using a redux + routing solution or just React Router. 7 | 8 | **Note:** make sure you add `redux-thunk` to your store's middleware or else the following will not work 9 | 10 | #### Using `react-router-redux` or `redux-router` and dispatching an extra redux action in the wrapper 11 | ```js 12 | import { replace } from 'react-router-redux'; // Or your redux-router equivalent 13 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history3/redirect' 14 | import addNotification from './notificationActions'; 15 | 16 | // Admin Authorization, redirects non-admins to /app 17 | export const userIsAdmin = connectedReduxRedirect({ 18 | redirectPath: '/app', 19 | allowRedirectBack: false, 20 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 21 | redirectAction: (newLoc) => (dispatch) => { 22 | dispatch(replace(newLoc)); 23 | dispatch(addNotification({ message: 'Sorry, you are not an administrator' })); 24 | }, 25 | wrapperDisplayName: 'UserIsAdmin' 26 | }) 27 | ``` 28 | 29 | #### Using React Router with history singleton and extra redux action 30 | ```js 31 | import { browserHistory } from 'react-router'; // react router 3 32 | import addNotification from './notificationActions'; 33 | 34 | // Admin Authorization, redirects non-admins to /app 35 | export const userIsAdmin = connectedReduxRedirect({ 36 | redirectPath: '/app', 37 | allowRedirectBack: false, 38 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 39 | redirectAction: (newLoc) => (dispatch) => { 40 | browserHistory.replace(newLoc); 41 | dispatch(addNotification({ message: 'Sorry, you are not an administrator' })); 42 | }, 43 | wrapperDisplayName: 'UserIsAdmin' 44 | }) 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/Getting-Started/HidingAlternate.md: -------------------------------------------------------------------------------- 1 | # Hiding and Displaying Alternate Components 2 | 3 | In addition to controlling what pages users can access in an application, another common requirement is to hide or display different elements on the page depending on the user's permissions. redux-auth-wrapper also provides an HOC that makes this easy to do in your application. 4 | 5 | ## Hiding a Component 6 | 7 | If you want to hide a component, you can import the `connectedAuthWrapper` HOC. When the `authenticatedSelector` returns true, the wrapped component will be rendered and passed all props from the parent. When the `authenticatedSelector` returns false, no component will be rendered. 8 | 9 | Here is an example that hides a link from a non-admin user. 10 | ```js 11 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 12 | 13 | const visibleOnlyAdmin = connectedAuthWrapper({ 14 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 15 | wrapperDisplayName: 'VisibleOnlyAdmin', 16 | }) 17 | 18 | // Applying to a function component for simplicity but could be Class or createClass component 19 | const AdminOnlyLink = visibleOnlyAdmin(() => Admin Section) 20 | ``` 21 | 22 | ## Displaying an Alternate Component 23 | 24 | You can also display a component when the `authenticatedSelector` returns false. Simply pass the `FailureComponent` property to the `authWrapper`. 25 | 26 | ```js 27 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 28 | 29 | const visibleOnlyAdmin = connectedAuthWrapper({ 30 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 31 | wrapperDisplayName: 'AdminOrHomeLink', 32 | FailureComponent: () => Home Section 33 | }) 34 | 35 | // Applying to a function component for simplicity but could be Class or createClass component 36 | const AdminOnlyLink = visibleOnlyAdmin(() => Admin Section) 37 | ``` 38 | 39 | You can also easily wrap the call to `authWrapper` in a function to make it more flexible to apply throughout your code: 40 | 41 | ```js 42 | const adminOrElse = (Component, FailureComponent) => connectedAuthWrapper({ 43 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 44 | wrapperDisplayName: 'AdminOrElse', 45 | FailureComponent 46 | })(Component) 47 | 48 | // Show Admin dashboard to admins and user dashboard to regular users 49 | 50 | ``` 51 | 52 | ## Unconnected Wrapper 53 | 54 | If you don't want to have redux-auth-wrapper connect your selector automatically for you, you can use the un-connected version. This might be useful if you are already connecting the component and dont want the extra overhead of another `connect`, or want to pass the props in via traditional state. 55 | 56 | ```js 57 | import authWrapper from 'redux-auth-wrapper/authWrapper' 58 | 59 | const visibleOnlyAdmin = authWrapper({ 60 | wrapperDisplayName: 'VisibleOnlyAdmin', 61 | }) 62 | 63 | // Applying to a function component for simplicity but could be Class or createClass component 64 | const AdminOnlyLink = visibleOnlyAdmin(() => Admin Section) 65 | 66 | class MyComponent extends Component { 67 | 68 | ... 69 | 70 | render() { 71 |
72 | 73 |
74 | } 75 | 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/Getting-Started/NestingWrappers.md: -------------------------------------------------------------------------------- 1 | # Nesting Wrappers 2 | 3 | Both the redirection HOCs and `authWrapper` can be nested (composed) together to create additional constraints for auth. This is especially useful when you want to provide authorization checks, such as checking a user is logged in and also an administrator. 4 | 5 | For example, we can create a button that will only display for logged in users named Bob: 6 | 7 | ```js 8 | const onlyBob = authWrapper({ 9 | authenticatedSelector: state => state.user.firstName === 'Bob' 10 | wrapperDisplayName: 'UserIsOnlyBob', 11 | }) 12 | 13 | const OnlyBobButton = onlyBob(MyButton) 14 | ``` 15 | 16 | When nesting redirection HOCs, it is important to pay attention to the order of the nesting, especially if you are using a different `redirectPath` or `allowRedirectBack`. Consider the following: 17 | 18 | ``` js 19 | const userIsAuthenticated = connectedRouterRedirect({ 20 | redirectPath: '/login', 21 | authenticatedSelector: state => state.user !== null, 22 | wrapperDisplayName: 'UserIsAuthenticated' 23 | }) 24 | 25 | const userIsAdmin = connectedRouterRedirect({ 26 | authenticatedSelector: state => state.user.isAdmin, 27 | redirectPath: '/app', 28 | wrapperDisplayName: 'UserIsAdmin', 29 | allowRedirectBack: false 30 | }) 31 | 32 | // Now to secure the component: first check if the user is authenticated, and then check if the user is an admin 33 | 34 | ``` 35 | 36 | Because the `userIsAuthenticated` check happens first, if the users aren't logged in they will be sent to the `/login` first, skipping the admin check all together. Then once they've authenticated, they can be checked for if they are an admin. Otherwise, they would be sent to `/app` which might also be protected by a `userIsAuthenticated` and then get sent to `/login` from there (assuming another auth check). This would result in the user ending up at `/app` once they've authenticated, instead of at `/admin` if they are an admin but weren't logged in at the time. 37 | 38 | #### Chaining using compose 39 | 40 | Since Higher Order Components are functions, they can easily be chained together using `compose` to prevent accidentally applying them in the wrong order. `compose` can be imported from redux, or recompose: 41 | 42 | ```js 43 | import { compose } from 'redux' 44 | 45 | // userIsAuthenticated and userIsAdmin from above 46 | const userIsAdminChain = compose(userIsAuthenticated, userIsAdmin) 47 | 48 | // Now to secure the component, you don't have to think which order to apply! 49 | 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/Getting-Started/Overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ## Higher Order Components 4 | 5 | redux-auth-wrapper makes use of higher order components to decouple the rendering logic in your components from the permissions a user might have. If you are unfamiliar with using higher order components or where to apply them, please read below, otherwise skip to the [Tutorials](#tutorials). 6 | 7 | For a great read on what higher order components are, check out Dan Abramov's blog post: 8 | 9 | [Higher Order Components](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.ao9jjxx89). 10 | > A higher-order component is just a function that takes an existing component and returns another component that wraps it 11 | 12 | ### Where to apply 13 | 14 | Higher order components are extremely powerful tools for adding logic to your components and keeping them easy to understand. It is important, however, to apply HOCs in the proper place. Failure to do so can cause subtle bugs and performance problems in your code. 15 | 16 | In all of the following examples, we use the hoc `authWrapper`, but this advice applies for all HOCs (even ones not from redux-auth-wrapper). 17 | 18 | #### Safe to Apply 19 | 20 | Directly inside ReactDOM.render: 21 | ```js 22 | ReactDOM.render( 23 | 24 | 25 | 26 | 27 | ... 28 | 29 | 30 | , 31 | document.getElementById('mount') 32 | ) 33 | ``` 34 | 35 | Separate route config file: 36 | ```js 37 | const routes = ( 38 | 39 | 40 | ... 41 | 42 | ) 43 | 44 | ReactDOM.render( 45 | 46 | 47 | {routes} 48 | 49 | , 50 | document.getElementById('mount') 51 | ) 52 | ``` 53 | 54 | Applied in the component file (es7): 55 | ```js 56 | @authWrapper 57 | export default class MyComponent extends Component { 58 | ... 59 | } 60 | ``` 61 | 62 | Applied in the component file (es6): 63 | ```js 64 | class MyComponent extends Component { 65 | ... 66 | } 67 | export default UserIsAuthenticated(MyComponent) 68 | ``` 69 | 70 | Applied outside the component file: 71 | ```js 72 | import MyComponent from './component/mycomponent.js' 73 | 74 | const MyAuthComponent = authWrapper(MyComponent) 75 | ``` 76 | 77 | #### Not Safe to Apply 78 | 79 | The following are all not safe because they create a new component over and over again, preventing react from considering these the "same" component and causing mounting/unmounting loops. 80 | 81 | Inside of render: 82 | ```js 83 | import MyComponent from './component/MyComponent.js' 84 | 85 | class MyParentComponent extends Component { 86 | render() { 87 | const MyAuthComponent = authWrapper(MyComponent) 88 | return 89 | } 90 | } 91 | ``` 92 | 93 | Inside of any `getComponent` (react router 3): 94 | ```js 95 | const routes = ( 96 | 97 | { 98 | cb(null, authWrapper(Foo)) 99 | }} /> 100 | ... 101 | 102 | ) 103 | ``` 104 | 105 | ## Tutorials 106 | 107 | * [React Router 3 Tutorial](ReactRouter3.md) 108 | * [React Router 4 Tutorial](ReactRouter4.md) 109 | * [React Native Tutorial](ReactNative.md) 110 | * [Hiding and Alternative Components](HidingAlternate.md) 111 | * [Nesting Wrappers](NestingWrappers.md) 112 | -------------------------------------------------------------------------------- /docs/Getting-Started/ReactNative.md: -------------------------------------------------------------------------------- 1 | # React Native Redirection 2 | 3 | Using redux-auth-wrapper with React Native? Please help improve these docs and the examples by submitting a Pull Request! 4 | -------------------------------------------------------------------------------- /docs/Getting-Started/ReactRouter3.md: -------------------------------------------------------------------------------- 1 | # React Router 3 Redirection 2 | 3 | At first glance, it appears that redux-auth-wrapper is unnecessary in React Router 3 because of the [onEnter](https://github.com/ReactTraining/react-router/blob/v3/docs/API.md#onenternextstate-replace-callback) method. 4 | 5 | `onEnter` is great, and useful in certain situations. However, here are some common auth problems `onEnter` does not solve: 6 | * Decide authentication/authorization from redux store data (there are some [workarounds](https://github.com/CrocoDillon/universal-react-redux-boilerplate/blob/master/src/routes.js#L8)) 7 | * Recheck auth when the store changes but not the current route (not possible!) 8 | 9 | Instead, we can use redux-auth-wrapper to protect React Router 3 routes with auth checks to more easily interact with our redux store. 10 | 11 | ## Securing a Route 12 | 13 | To add redirection to your app with React Router 3, import the following helper: 14 | ```js 15 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history3/redirect' 16 | ``` 17 | 18 | The `connectedRouterRedirect` will build the redirect higher order component, but we have to first pass in a config object. The wrapper needs to know how to determine if a user is allowed to access the route. 19 | 20 | ```js 21 | const userIsAuthenticated = connectedRouterRedirect({ 22 | // The url to redirect user to if they fail 23 | redirectPath: '/login', 24 | // Determine if the user is authenticated or not 25 | authenticatedSelector: state => state.user !== null, 26 | // A nice display name for this check 27 | wrapperDisplayName: 'UserIsAuthenticated' 28 | }) 29 | ``` 30 | 31 | `userIsAuthenticated` is a Higher Order Component, so we can apply it to the component we want to protect. You can do this in many places, see [where to apply the wrappers](Overview.md#where-to-apply) for more details. In our case, we apply it directly in the route definition. 32 | 33 | ```js 34 | 35 | ``` 36 | 37 | When the user navigates to `/profile`, one of the following occurs: 38 | 39 | 1. If The `state.user` is null: 40 | 41 | The user is redirected to `/login?redirect=%2Fprofile` 42 | 43 | *Notice the url contains the query parameter `redirect` for sending the user back to after you log them into your app* 44 | 2. Otherwise: 45 | 46 | The `` component is rendered 47 | 48 | ## Redirecting from Login 49 | 50 | We've only done half of the work however. When a user logs into the login page, we want to send them back to `/profile`. Additionally, if a user is already logged in, but navigates to our login page, we may want to send them to a landing page (`/landing`). Luckily we can easily do both of these with another wrapper. 51 | 52 | ```js 53 | import locationHelperBuilder from 'redux-auth-wrapper/history3/locationHelper' 54 | 55 | const locationHelper = locationHelperBuilder({}) 56 | 57 | const userIsNotAuthenticated = connectedRouterRedirect({ 58 | // This sends the user either to the query param route if we have one, or to the landing page if none is specified and the user is already logged in 59 | redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/landing', 60 | // This prevents us from adding the query parameter when we send the user away from the login page 61 | allowRedirectBack: false, 62 | // Determine if the user is authenticated or not 63 | authenticatedSelector: state => state.user === null, 64 | // A nice display name for this check 65 | wrapperDisplayName: 'UserIsNotAuthenticated' 66 | }) 67 | ``` 68 | 69 | ```js 70 | 71 | 72 | ``` 73 | 74 | The `locationHelper` requires the `location` object in props. If the component is not rendered as part of a route component then you must use the `withRouter` HOC from `react-router`: 75 | 76 | ```js 77 | withRouter(userIsNotAuthenticated(Login)) 78 | ``` 79 | 80 | ## Displaying an AuthenticatingComponent Component 81 | 82 | Its often useful to display some sort of loading component or animation when you are checking if the user's credentials are valid or if the user is already logged in. We can add a loading component to both our Login and Profile page easily: 83 | 84 | When `authenticatingSelector` returns true, no redirection will be performed and the the specified `AuthenticatingComponent` will be displayed. If no `AuthenticatingComponent` is specified, then no component will be rendered (null). 85 | 86 | ```js 87 | const userIsAuthenticated = connectedRouterRedirect({ 88 | redirectPath: '/login', 89 | authenticatedSelector: state => state.user !== null, 90 | wrapperDisplayName: 'UserIsAuthenticated' 91 | // Returns true if the user auth state is loading 92 | authenticatingSelector: state => state.user.isLoading, 93 | // Render this component when the authenticatingSelector returns true 94 | AuthenticatingComponent: LoadingSpinner, 95 | }) 96 | ``` 97 | 98 | You can also add an `authenticatingSelector` and `AuthenticatingComponent` 99 | 100 | ## Integrating with redux-based routing 101 | 102 | If you want to dispatch a redux action to perform navigation instead of interacting directly with the history/router object then you can pass the redux action creator to `redirectAction`. Note that using `redirectAction` is not required if you use redux-based or redux-integrated routing, it only changes how the route change is triggered in the client. 103 | 104 | To do this, swap out the import of `connectedRouterRedirect` for `connectedReduxRedirect` and pass the `redirectAction` parameter to the config object: 105 | 106 | ```js 107 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history3/redirect' 108 | import { routerActions } from 'react-router-redux' 109 | 110 | const userIsAuthenticated = connectedReduxRedirect({ 111 | redirectPath: '/login', 112 | authenticatedSelector: state => state.user !== null, 113 | wrapperDisplayName: 'UserIsAuthenticated', 114 | // This should be a redux action creator 115 | redirectAction: routerActions.replace, 116 | }) 117 | ``` 118 | 119 | ## Next Steps 120 | 121 | Check out the [examples](https://github.com/mjrussell/redux-auth-wrapper/tree/master/examples) or browse the [API documentation](/docs/API.md). If you are using server side rendering (SSR) with React Router 3, you should also check out the [Server Side Rendering](/docs/AdvancedUsage/ReactRouter3.md) documentation. 122 | -------------------------------------------------------------------------------- /docs/Getting-Started/ReactRouter4.md: -------------------------------------------------------------------------------- 1 | # React Router 4/5 Redirection 2 | 3 | _note: this guide refers mainlys to React Router 4. React Router 5 is the same API and redux-auth-wrapper 3.x is fully compatible with React Router 5._ 4 | 5 | React Router 4 removed `onEnter` and `onChange` in favor of performing those routing logic actions inside component life cycle methods. This is what redux-auth-wrapper already does with Higher Order Components! 6 | 7 | Using redux-auth-wrapper with React Router 4 is very similar to React Router 3 with a few changes. 8 | 9 | ## Securing a Route 10 | 11 | To add redirection to your app with React Router 4, import the following helper: 12 | ```js 13 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history4/redirect' 14 | ``` 15 | 16 | The `connectedRouterRedirect` will build the redirect higher order component, but we have to first pass in a config object. The wrapper needs to know how to determine if a user is allowed to access the route. 17 | 18 | ```js 19 | const userIsAuthenticated = connectedRouterRedirect({ 20 | // The url to redirect user to if they fail 21 | redirectPath: '/login', 22 | // If selector is true, wrapper will not redirect 23 | // For example let's check that state contains user data 24 | authenticatedSelector: state => state.user.data !== null, 25 | // A nice display name for this check 26 | wrapperDisplayName: 'UserIsAuthenticated' 27 | }) 28 | ``` 29 | 30 | `userIsAuthenticated` is a Higher Order Component, so we can apply it to the component we want to protect. You can do this in many places, see [where to apply the wrappers](Overview.md#where-to-apply) for more details. In our case, we apply it directly in the route definition. 31 | 32 | ```js 33 | 34 | ``` 35 | 36 | When the user navigates to `/profile`, one of the following occurs: 37 | 38 | 1. If The `state.user` is null: 39 | 40 | The user is redirected to `/login?redirect=%2Fprofile` 41 | 42 | *Notice the url contains the query parameter `redirect` for sending the user back to after you log them into your app* 43 | 2. Otherwise: 44 | 45 | The `` component is rendered 46 | 47 | ## Redirecting from Login 48 | 49 | We've only done half of the work however. When a user logs into the login page, we want to send them back to `/profile`. Additionally, if a user is already logged in, but navigates to our login page, we may want to send them to a landing page (`/landing`). Luckily we can easily do both of these with another wrapper. 50 | 51 | ```js 52 | import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper' 53 | 54 | const locationHelper = locationHelperBuilder({}) 55 | 56 | const userIsNotAuthenticated = connectedRouterRedirect({ 57 | // This sends the user either to the query param route if we have one, or to the landing page if none is specified and the user is already logged in 58 | redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/landing', 59 | // This prevents us from adding the query parameter when we send the user away from the login page 60 | allowRedirectBack: false, 61 | // If selector is true, wrapper will not redirect 62 | // So if there is no user data, then we show the page 63 | authenticatedSelector: state => state.user.data === null, 64 | // A nice display name for this check 65 | wrapperDisplayName: 'UserIsNotAuthenticated' 66 | }) 67 | ``` 68 | 69 | ```js 70 | 71 | 72 | ``` 73 | 74 | The `locationHelper` requires the `location` object in props. If the component is not rendered as part of a route component then you must use the `withRouter` HOC from `react-router`: 75 | 76 | ```js 77 | withRouter(userIsNotAuthenticated(Login)) 78 | ``` 79 | 80 | ## Displaying an AuthenticatingComponent Component 81 | 82 | Its often useful to display some sort of loading component or animation when you are checking if the user's credentials are valid or if the user is already logged in. We can add a loading component to both our Login and Profile page easily: 83 | 84 | When `authenticatingSelector` returns true, no redirection will be performed and the the specified `AuthenticatingComponent` will be displayed. If no `AuthenticatingComponent` is specified, then no component will be rendered (null). 85 | 86 | ```js 87 | const userIsAuthenticated = connectedRouterRedirect({ 88 | redirectPath: '/login', 89 | authenticatedSelector: state => state.user.data !== null, 90 | wrapperDisplayName: 'UserIsAuthenticated', 91 | // Returns true if the user auth state is loading 92 | authenticatingSelector: state => state.user.isLoading, 93 | // Render this component when the authenticatingSelector returns true 94 | AuthenticatingComponent: LoadingSpinner 95 | }) 96 | ``` 97 | 98 | You can also add an `authenticatingSelector` and `AuthenticatingComponent` 99 | 100 | ## Integrating with redux-based routing 101 | 102 | If you want to dispatch a redux action to perform navigation instead of interacting directly with the history/router object then you can pass the redux action creator to `redirectAction`. Note that using `redirectAction` is not required if you use redux-based or redux-integrated routing, it only changes how the route change is triggered in the client. 103 | 104 | To do this, swap out the import of `connectedRouterRedirect` for `connectedReduxRedirect` and pass the `redirectAction` parameter to the config object: 105 | 106 | ```js 107 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history4/redirect' 108 | import { replace } from 'connected-react-router' 109 | 110 | const userIsAuthenticated = connectedReduxRedirect({ 111 | redirectPath: '/login', 112 | authenticatedSelector: state => state.user !== null, 113 | wrapperDisplayName: 'UserIsAuthenticated', 114 | // This should be a redux action creator 115 | redirectAction: replace, 116 | }) 117 | ``` 118 | 119 | ## Next Steps 120 | 121 | Check out the [examples](https://github.com/mjrussell/redux-auth-wrapper/tree/master/examples) or browse the [API documentation](/docs/API.md). 122 | -------------------------------------------------------------------------------- /docs/Migrating.md: -------------------------------------------------------------------------------- 1 | # Migrating from Version 1.x to Version 2.x 2 | 3 | ## Motivation for 2.x 4 | 5 | redux-auth-wrapper has really changed a lot since it was first designed as a small specialized utility for handling redirection with react-router-redux. Since then, it has been adopted by many developers and they've used it in numerous unforeseen and useful ways such as hiding components, displaying alternate components, and integrating with other routers. Additionally, redux-auth-wrapper needed to support two version of React Router (3 and 4) which had very different APIs. Therefore, version 2.x breaks redux-auth-wrapper into many more small, composable pieces. redux-auth-wrapper still provides a simple import for those looking to get started quickly with React Router, but also allows for developers to import the building blocks to create redirection wrappers that work with any router (or history directly). You can even use redux-auth-wrapper 2.x in a project without redux or even routing. 6 | 7 | The largest change is that wrappers that perform redirection have been split from those that hide or display alternate components using `FailureComponent`. 8 | This made practical sense since using a FailureComponent disabled the redirection, yet the wrapper would still complain 9 | about missing redirection helpers like `history` even when they would not be used. Now you can use Failure Component wrappers 10 | without even a router. 11 | 12 | ## Migrating redirection wrappers 13 | 14 | The main changes are the following: 15 | * Combined `authSelector` and `predicate` into a single `authenticatedSelector` 16 | * No longer passed `authData` as a prop to child components. This was the return value of `authSelector`. If you need your auth data, just connect it at a lower level. 17 | * renamed `LoadingComponent` to `AuthenticatingComponent` 18 | * renamed `failureRedirectPath` to `redirectPath` 19 | * `redirectPath` no longer defaults to `/login` 20 | * removed `FailureComponent` from the redirect helper, see [Migrating failure and alternative components](#migrating-failure-and-alternative-components) for details 21 | * Removed `mapProps`. If you need to prevent passing down any props from redux-auth-wrapper, use `mapProps` from recompose. 22 | 23 | Previously: 24 | 25 | v1.x: 26 | ```js 27 | import { UserAuthWrapper } from 'redux-auth-wrapper' 28 | 29 | const UserIsAuthenticated = UserAuthWrapper({ 30 | authSelector: state => state.user.data, 31 | authenticatingSelector: state => state.user.isLoading, 32 | LoadingComponent: Loading, 33 | redirectAction: routerActions.replace, 34 | wrapperDisplayName: 'UserIsAuthenticated' 35 | }) 36 | ``` 37 | 38 | v2.x: 39 | ```js 40 | // NOTE: use history3 because coming from React Router 2/3. If planning to upgrade to React Router 4 use history4 41 | import { connectedReduxRedirect } from 'redux-auth-wrapper/history3/redirect' 42 | 43 | export const userIsAuthenticated = connectedReduxRedirect({ 44 | redirectPath: '/login', 45 | authenticatedSelector: state => state.user.data !== null, 46 | authenticatingSelector: state => state.user.isLoading, 47 | AuthenticatingComponent: Loading, 48 | redirectAction: routerActions.replace, 49 | wrapperDisplayName: 'UserIsAuthenticated' 50 | }) 51 | ``` 52 | 53 | **Note:** If not using `redirectAction`, import `connectedRouterRedirect` instead. 54 | 55 | ## Migrating failure and alternative components 56 | 57 | * Combined `authSelector` and `predicate` into a single `authenticatedSelector` 58 | * FailureComponent is optional now, not specifying it will render nothing (null) when the `authenticatedSelector` returns false 59 | * All properties besides `authenticatedSelector`, `authenticatingSelector`, `FailureComponent`, and `wrapperDisplayName` have been removed 60 | 61 | ### Hiding Components 62 | 63 | v1.x 64 | ```js 65 | import { UserAuthWrapper } from 'redux-auth-wrapper' 66 | 67 | 68 | const VisibleOnlyAdmin = UserAuthWrapper({ 69 | authSelector: state => state.user, 70 | wrapperDisplayName: 'VisibleOnlyAdmin', 71 | predicate: user => user.isAdmin, 72 | FailureComponent: null 73 | }) 74 | 75 | // Applying to a function component for simplicity but could be Class or createClass component 76 | const AdminOnlyLink = VisibleOnlyAdmin(() => Admin Section) 77 | ``` 78 | 79 | v2.x 80 | ```js 81 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 82 | 83 | const visibleOnlyAdmin = authWrapper({ 84 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 85 | wrapperDisplayName: 'VisibleOnlyAdmin', 86 | }) 87 | ``` 88 | 89 | ### Alternate Components 90 | 91 | v1.x 92 | ```js 93 | import { UserAuthWrapper } from 'redux-auth-wrapper' 94 | 95 | const AdminOrElse = (Component, FailureComponent) => UserAuthWrapper({ 96 | authSelector: state => state.user, 97 | wrapperDisplayName: 'AdminOrElse', 98 | predicate: user => user.isAdmin, 99 | FailureComponent 100 | })(Component) 101 | 102 | // Show Admin dashboard to admins and user dashboard to regular users 103 | 104 | ``` 105 | 106 | v2.x 107 | ```js 108 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 109 | 110 | const adminOrElse = (Component, FailureComponent) => connectedAuthWrapper({ 111 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 112 | wrapperDisplayName: 'AdminOrElse', 113 | FailureComponent 114 | })(Component) 115 | 116 | // Show Admin dashboard to admins and user dashboard to regular users 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting and Common Issues 2 | 3 | Having trouble with redux-auth-wrapper? Check out the following common issues. 4 | 5 | #### Applying the HOC 6 | 7 | Make sure that when using the helpers from redux-auth-wrapper that you are applying the HOC to your component and in the right location (see [where to apply the wrappers](Overview.md#where-to-apply) for more details). Most imports from this library are HOC builders, requiring first a configuration object. For instance you shouldn't be applying the `connectedRouterRedirect` directly to a component: 8 | 9 | Incorrect: 10 | ```js 11 | const ProtectedComponent = connectedRouterRedirect(MyComponent) 12 | ``` 13 | 14 | Correct: 15 | ```js 16 | const userIsAuthenticated = connectedRouterRedirect({ 17 | redirectPath: '/login', 18 | authenticatedSelector: state => state.user !== null 19 | }) 20 | 21 | const ProtectedComponent = userIsAuthenticated(MyComponent) 22 | ``` 23 | 24 | Also, please be sure that you've applied the HOC in a proper location. Check out the documentation on [where to apply auth wrappers](/docs/Getting-Started/Overview.md#where-to-apply). 25 | 26 | #### Rendering the wrapped component 27 | 28 | Also remember that the result of an HOC being applied to a Component is a new Component, so you cannot render it without instantiating it: 29 | 30 | Incorrect: 31 | ```js 32 | const visibleOnlyAdmin = authWrapper({ 33 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 34 | wrapperDisplayName: 'AdminOrHomeLink', 35 | FailureComponent: () => Home Section 36 | }) 37 | 38 | const AdminOnlyLink = visibleOnlyAdmin(() => Admin Section) 39 | 40 | class MyComponent extends Component { 41 | render() { 42 | return ( 43 |
44 | {AdminOnlyLink} 45 |
46 | ) 47 | 48 | } 49 | } 50 | ``` 51 | 52 | Correct: 53 | ```js 54 | const visibleOnlyAdmin = authWrapper({ 55 | authenticatedSelector: state => state.user !== null && state.user.isAdmin, 56 | wrapperDisplayName: 'AdminOrHomeLink', 57 | FailureComponent: () => Home Section 58 | }) 59 | 60 | const AdminOnlyLink = visibleOnlyAdmin(() => Admin Section) 61 | 62 | class MyComponent extends Component { 63 | render() { 64 | return ( 65 |
66 | 67 |
68 | ) 69 | 70 | } 71 | } 72 | ``` 73 | 74 | #### Replace of undefined 75 | 76 | The redirect helpers make us of the `history` object from React Router. In most cases, this is passed down because the wrapped component is rendered as a child of ``. However if you render the component elsewhere you might get `Uncaught TypeError: Cannot read property 'replace' of undefined`. This likely means the `history` object was not passed to the component. You can solve this by using the `withRouter` higher order component: 77 | 78 | ```js 79 | import { withRouter } from 'react-router'; 80 | const userIsAuthenticated = connectedRouterRedirect({ 81 | redirectPath: '/login', 82 | authenticatedSelector: state => state.user.data. !== null 83 | }) 84 | 85 | const ProtectedComponent = withRouter(userIsAuthenticated(MyComponent)) 86 | ``` 87 | -------------------------------------------------------------------------------- /examples/react-router-3/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/react-router-3/README.md: -------------------------------------------------------------------------------- 1 | redux-auth-wrapper react router 3 example 2 | ================================= 3 | 4 | This is an example of redux-auth-wrapper that uses `authenticatingSelector` with `LoadingComponent` 5 | to show a loading screen while the user logs in. This example also demonstrates how to use the UserAuthWrapper for 6 | wrapping the Login Component in an HOC. 7 | 8 | This example uses React-Router 3.x and React-Router-Redux 4.x. 9 | 10 | **To run, follow these steps:** 11 | 12 | 1. Go to the root of this project (up two folders) and run `npm install && npm run build` 13 | 2. In this folder, run `npm install` 14 | 3. In this folder, `npm start` 15 | 4. `Browse to http://localhost:8080` 16 | 17 | Login as any user to access the protected page `foo`. 18 | Login with the admin box check to access the admin section. 19 | Logout from any protected page to get redirect back to the login page. 20 | -------------------------------------------------------------------------------- /examples/react-router-3/actions/user.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | export const login = data => dispatch => { 4 | dispatch({ 5 | type: constants.USER_LOGGING_IN 6 | }) 7 | // Wait 2 seconds before "logging in" 8 | setTimeout(() => { 9 | dispatch({ 10 | type: constants.USER_LOGGED_IN, 11 | payload: data 12 | }) 13 | }, 2000) 14 | } 15 | 16 | export function logout() { 17 | return { 18 | type: constants.USER_LOGGED_OUT 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-router-3/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import { Router, Route, IndexRoute, browserHistory } from 'react-router' 6 | import { routerReducer, syncHistoryWithStore, routerMiddleware } from 'react-router-redux' 7 | import thunkMiddleware from 'redux-thunk' 8 | 9 | import * as reducers from './reducers' 10 | import { App, Home, Foo, Admin, Login } from './components' 11 | import { userIsAuthenticated, userIsAdmin, userIsNotAuthenticated } from './auth' 12 | 13 | const baseHistory = browserHistory 14 | const routingMiddleware = routerMiddleware(baseHistory) 15 | const reducer = combineReducers(Object.assign({}, reducers, { 16 | routing: routerReducer 17 | })) 18 | 19 | const enhancer = compose( 20 | // Middleware you want to use in development: 21 | applyMiddleware(thunkMiddleware, routingMiddleware), 22 | ) 23 | 24 | // Note: passing enhancer as the last argument requires redux@>=3.1.0 25 | const store = createStore(reducer, enhancer) 26 | const history = syncHistoryWithStore(baseHistory, store) 27 | 28 | ReactDOM.render( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | , 39 | document.getElementById('mount') 40 | ) 41 | -------------------------------------------------------------------------------- /examples/react-router-3/auth.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from 'redux-auth-wrapper/history3/locationHelper' 2 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history3/redirect' 3 | import { routerActions } from 'react-router-redux' 4 | 5 | import { Loading } from './components' 6 | 7 | const locationHelper = locationHelperBuilder({}) 8 | 9 | export const userIsAuthenticated = connectedRouterRedirect({ 10 | redirectPath: '/login', 11 | authenticatedSelector: state => state.user.data !== null, 12 | authenticatingSelector: state => state.user.isLoading, 13 | AuthenticatingComponent: Loading, 14 | redirectAction: routerActions.replace, 15 | wrapperDisplayName: 'UserIsAuthenticated' 16 | }) 17 | 18 | export const userIsAdmin = connectedRouterRedirect({ 19 | redirectPath: '/', 20 | allowRedirectBack: false, 21 | authenticatedSelector: state => state.user.data !== null && state.user.data.isAdmin, 22 | redirectAction: routerActions.replace, 23 | wrapperDisplayName: 'UserIsAdmin' 24 | }) 25 | 26 | export const userIsNotAuthenticated = connectedRouterRedirect({ 27 | redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/foo', 28 | allowRedirectBack: false, 29 | // Want to redirect the user when they are done loading and authenticated 30 | authenticatedSelector: state => state.user.data === null && state.user.isLoading === false, 31 | redirectAction: routerActions.replace, 32 | wrapperDisplayName: 'UserIsNotAuthenticated' 33 | }) 34 | -------------------------------------------------------------------------------- /examples/react-router-3/components/Admin.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Admin = ({ authData }) => { 5 | return
{`Welcome admin user: ${authData.name}`}
6 | } 7 | 8 | export default connect(state => ({ authData: state.user.data }))(Admin) 9 | -------------------------------------------------------------------------------- /examples/react-router-3/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { connect } from 'react-redux' 4 | import { logout } from '../actions/user' 5 | 6 | function App({ children, logout }) { 7 | return ( 8 |
9 |
10 | Links: 11 | {' '} 12 | Home 13 | {' '} 14 | {'Foo (Login Required)'} 15 | {' '} 16 | {'Admin'} 17 | {' '} 18 | Login 19 | {' '} 20 | 21 |
22 |
{children}
23 |
24 | ) 25 | } 26 | 27 | export default connect(false, { logout })(App) 28 | -------------------------------------------------------------------------------- /examples/react-router-3/components/Foo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Foo = ({ authData }) => { 5 | return ( 6 |
{`I am Foo! Welcome ${authData.name}`}
7 | ) 8 | } 9 | export default connect(state => ({ authData: state.user.data }))(Foo) 10 | -------------------------------------------------------------------------------- /examples/react-router-3/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

{"Welcome! Why dont you login and check out Foo? Or log in as an admin and click Admin"}

7 |

{"Or just try to navigate there and you will be redirected"}

8 |

{"Dont forget to try logging out on any page!"}

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-router-3/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Loading() { 4 | return
Logging you in...
5 | } 6 | -------------------------------------------------------------------------------- /examples/react-router-3/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | import { login } from '../actions/user' 6 | 7 | export class LoginContainer extends Component { 8 | 9 | static propTypes = { 10 | login: PropTypes.func.isRequired 11 | }; 12 | 13 | onClick = (e) => { 14 | e.preventDefault() 15 | this.props.login({ 16 | name: this.refs.name.value, 17 | isAdmin: this.refs.admin.checked 18 | }) 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |

Enter your name

25 | 26 |
27 | {'Admin?'} 28 | 29 |
30 | 31 |
32 | ) 33 | } 34 | 35 | } 36 | export default connect(null, { login })(LoginContainer) 37 | -------------------------------------------------------------------------------- /examples/react-router-3/components/index.js: -------------------------------------------------------------------------------- 1 | export App from './App' 2 | export Home from './Home' 3 | export Foo from './Foo' 4 | export Admin from './Admin' 5 | export Login from './Login' 6 | export Loading from './Loading' 7 | -------------------------------------------------------------------------------- /examples/react-router-3/constants.js: -------------------------------------------------------------------------------- 1 | export const USER_LOGGING_IN = 'USER_LOGGING_IN' 2 | export const USER_LOGGED_IN = 'USER_LOGGED_IN' 3 | export const USER_LOGGED_OUT = 'USER_LOGGED_OUT' 4 | -------------------------------------------------------------------------------- /examples/react-router-3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | redux-auth-wrapper basic example 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/react-router-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raw-loading-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "react-router": "3.2.5", 6 | "react-router-redux": "4.0.8", 7 | "redux-thunk": "2.3.0" 8 | }, 9 | "devDependencies": { 10 | "babel-loader": "7.1.5", 11 | "html-webpack-plugin": "3.2.0", 12 | "webpack": "4.41.5", 13 | "webpack-cli": "3.3.10", 14 | "webpack-dev-server": "3.10.1" 15 | }, 16 | "scripts": { 17 | "start": "webpack-dev-server --config webpack.config.babel.js --progress" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-router-3/reducers/index.js: -------------------------------------------------------------------------------- 1 | import user from './user' 2 | 3 | module.exports = { user } 4 | -------------------------------------------------------------------------------- /examples/react-router-3/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | const initialState = { 4 | data: null, 5 | isLoading: false 6 | } 7 | 8 | export default function userUpdate(state = initialState, { type, payload }) { 9 | switch (type) { 10 | case constants.USER_LOGGING_IN: 11 | return { ...initialState, isLoading: true } 12 | case constants.USER_LOGGED_IN: 13 | return { data: payload, isLoading: false } 14 | case constants.USER_LOGGED_OUT: 15 | return initialState 16 | default: 17 | return state 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-router-3/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import HtmlWebpackPlugin from 'html-webpack-plugin' 4 | 5 | module.exports = { 6 | entry: './app.js', 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | devServer: { 12 | inline: true, 13 | historyApiFallback: true, 14 | stats: { 15 | colors: true, 16 | hash: false, 17 | version: false, 18 | chunks: false, 19 | children: false 20 | } 21 | }, 22 | module: { 23 | rules: [ { 24 | test: /\.js$/, 25 | loaders: [ 'babel-loader' ], 26 | exclude: /node_modules/, 27 | include: __dirname 28 | } ] 29 | }, 30 | plugins: [ 31 | new HtmlWebpackPlugin({ 32 | template: 'index.html', // Load a custom template 33 | inject: 'body' // Inject all scripts into the body 34 | }) 35 | ] 36 | } 37 | 38 | // This will make the redux-auth-wrapper module resolve to the 39 | // latest src instead of using it from npm. Remove this if running 40 | // outside of the source. 41 | const lib = path.join(__dirname, '..', '..', 'lib') 42 | if (fs.existsSync(lib)) { 43 | // Use the latest src 44 | module.exports.resolve = { alias: { 'redux-auth-wrapper': lib } } 45 | // module.exports.module.loaders.push({ 46 | // test: /\.js$/, 47 | // loaders: [ 'babel' ], 48 | // include: lib 49 | // }) 50 | } else { 51 | throw "redux-auth-wrapper source not built. Run the following: 'pushd ../.. && rm -rf node_modules && yarn install && yarn run build && popd' and then rerun 'yarn start'" 52 | } 53 | -------------------------------------------------------------------------------- /examples/react-router-4/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/react-router-4/README.md: -------------------------------------------------------------------------------- 1 | redux-auth-wrapper react router 4 example 2 | ================================= 3 | 4 | This is an example of redux-auth-wrapper that uses `authenticatingSelector` with `LoadingComponent` 5 | to show a loading screen while the user logs in. This example also demonstrates how to use the UserAuthWrapper for 6 | wrapping the Login Component in an HOC. 7 | 8 | This example uses React-Router 4.x 9 | 10 | **To run, follow these steps:** 11 | 12 | 1. Go to the root of this project (up two folders) and run `npm install && npm run build` 13 | 2. In this folder, run `npm install` 14 | 3. In this folder, `npm start` 15 | 4. `Browse to http://localhost:8080` 16 | 17 | Login as any user to access the protected page `protected`. 18 | Login with the admin box check to access the admin section. 19 | Logout from any protected page to get redirect back to the login page. 20 | -------------------------------------------------------------------------------- /examples/react-router-4/actions/user.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | export const login = data => dispatch => { 4 | dispatch({ 5 | type: constants.USER_LOGGING_IN 6 | }) 7 | // Wait 2 seconds before "logging in" 8 | setTimeout(() => { 9 | dispatch({ 10 | type: constants.USER_LOGGED_IN, 11 | payload: data 12 | }) 13 | }, 2000) 14 | } 15 | 16 | export function logout() { 17 | return { 18 | type: constants.USER_LOGGED_OUT 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/react-router-4/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import thunkMiddleware from 'redux-thunk' 6 | 7 | import * as reducers from './reducers' 8 | import App from './components/App' 9 | 10 | const reducer = combineReducers(Object.assign({}, reducers, {})) 11 | 12 | const enhancer = compose( 13 | // Middleware you want to use in development: 14 | applyMiddleware(thunkMiddleware), 15 | ) 16 | 17 | // Note: passing enhancer as the last argument requires redux@>=3.1.0 18 | const store = createStore(reducer, enhancer) 19 | 20 | ReactDOM.render( 21 | 22 |
23 | 24 |
25 |
, 26 | document.getElementById('mount') 27 | ) 28 | -------------------------------------------------------------------------------- /examples/react-router-4/auth.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from 'redux-auth-wrapper/history4/locationHelper' 2 | import { connectedRouterRedirect } from 'redux-auth-wrapper/history4/redirect' 3 | import connectedAuthWrapper from 'redux-auth-wrapper/connectedAuthWrapper' 4 | 5 | import Loading from './components/Loading' 6 | 7 | const locationHelper = locationHelperBuilder({}) 8 | 9 | const userIsAuthenticatedDefaults = { 10 | authenticatedSelector: state => state.user.data !== null, 11 | authenticatingSelector: state => state.user.isLoading, 12 | wrapperDisplayName: 'UserIsAuthenticated' 13 | } 14 | 15 | export const userIsAuthenticated = connectedAuthWrapper(userIsAuthenticatedDefaults) 16 | 17 | export const userIsAuthenticatedRedir = connectedRouterRedirect({ 18 | ...userIsAuthenticatedDefaults, 19 | AuthenticatingComponent: Loading, 20 | redirectPath: '/login' 21 | }) 22 | 23 | export const userIsAdminRedir = connectedRouterRedirect({ 24 | redirectPath: '/', 25 | allowRedirectBack: false, 26 | authenticatedSelector: state => state.user.data !== null && state.user.data.isAdmin, 27 | predicate: user => user.isAdmin, 28 | wrapperDisplayName: 'UserIsAdmin' 29 | }) 30 | 31 | const userIsNotAuthenticatedDefaults = { 32 | // Want to redirect the user when they are done loading and authenticated 33 | authenticatedSelector: state => state.user.data === null && state.user.isLoading === false, 34 | wrapperDisplayName: 'UserIsNotAuthenticated' 35 | } 36 | 37 | export const userIsNotAuthenticated = connectedAuthWrapper(userIsNotAuthenticatedDefaults) 38 | 39 | export const userIsNotAuthenticatedRedir = connectedRouterRedirect({ 40 | ...userIsNotAuthenticatedDefaults, 41 | redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/protected', 42 | allowRedirectBack: false 43 | }) 44 | -------------------------------------------------------------------------------- /examples/react-router-4/components/Admin.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Admin = ({ authData }) => { 5 | return
{`Welcome admin user: ${authData.name}. You must be logged in as an admin if you are seeing this page.`}
6 | } 7 | 8 | export default connect(state => ({ authData: state.user.data }))(Admin) 9 | -------------------------------------------------------------------------------- /examples/react-router-4/components/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Tahoma, Verdana, Segoe, sans-serif; 4 | } 5 | .wrapper { 6 | display: flex; 7 | flex-direction: row; 8 | flex-wrap: wrap; 9 | } 10 | .navigation { 11 | flex: 1 0 60%; 12 | width: 60%; 13 | background-color: #263238; 14 | min-height: 30px; 15 | display: flex; 16 | } 17 | 18 | .navigation a, .authNavigation a { 19 | color: white; 20 | text-decoration: none; 21 | padding: 10px 10px 10px 10px; 22 | } 23 | .authNavigation { 24 | flex: 0 1 20%; 25 | width: 20%; 26 | background-color: #263238; 27 | min-height: 30px; 28 | display: flex; 29 | flex-direction: row-reverse; 30 | } 31 | .login { 32 | width: 50%; 33 | margin: 40px auto; 34 | } 35 | .login .username { 36 | -moz-appearance: none; 37 | -webkit-appearance: none; 38 | -webkit-box-align: center; 39 | -ms-flex-align: center; 40 | align-items: center; 41 | border: 1px solid transparent; 42 | border-radius: 3px; 43 | -webkit-box-shadow: none; 44 | box-shadow: none; 45 | display: -webkit-inline-box; 46 | display: -ms-inline-flexbox; 47 | display: inline-flex; 48 | font-size: 1rem; 49 | height: 2.25em; 50 | line-height: 1.5; 51 | padding: 5px 10px; 52 | border-color: #dbdbdb; 53 | color: #363636; 54 | -webkit-box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); 55 | box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); 56 | max-width: 100%; 57 | width: 100%; 58 | margin-bottom: 20px; 59 | } 60 | .checkbox { 61 | width: 100%; 62 | } 63 | .checkbox input { 64 | margin-right: 10px; 65 | } 66 | .button { 67 | -moz-appearance: none; 68 | -webkit-appearance: none; 69 | -webkit-box-align: center; 70 | -ms-flex-align: center; 71 | align-items: center; 72 | border: 1px solid transparent; 73 | border-radius: 3px; 74 | -webkit-box-shadow: none; 75 | box-shadow: none; 76 | display: -webkit-inline-box; 77 | display: -ms-inline-flexbox; 78 | display: inline-flex; 79 | font-size: 1rem; 80 | height: 2.25em; 81 | -webkit-box-pack: start; 82 | -ms-flex-pack: start; 83 | justify-content: flex-start; 84 | line-height: 1.5; 85 | padding-bottom: calc(0.375em - 1px); 86 | padding-left: calc(0.625em - 1px); 87 | padding-right: calc(0.625em - 1px); 88 | padding-top: calc(0.375em - 1px); 89 | position: relative; 90 | vertical-align: top; 91 | -webkit-touch-callout: none; 92 | -webkit-user-select: none; 93 | -moz-user-select: none; 94 | -ms-user-select: none; 95 | user-select: none; 96 | background-color: white; 97 | border-color: #dbdbdb; 98 | color: #363636; 99 | cursor: pointer; 100 | -webkit-box-pack: center; 101 | -ms-flex-pack: center; 102 | justify-content: center; 103 | padding-left: 0.75em; 104 | padding-right: 0.75em; 105 | text-align: center; 106 | white-space: nowrap; 107 | margin-top: 20px; 108 | background-color: #00d1b2; 109 | border-color: transparent; 110 | color: #fff; 111 | } 112 | .content { 113 | flex: 1 0 100%; 114 | padding: 10px; 115 | } 116 | 117 | .content h2 { 118 | font-family: Arial Black, Arial Bold, Gadget, sans-serif; 119 | margin-top: 0; 120 | } 121 | .active { 122 | background-color: #006064; 123 | } 124 | 125 | .code { 126 | font-family: courier; 127 | padding: 1em; 128 | border: 1px solid #ccc; 129 | } 130 | 131 | .username { 132 | text-decoration: none; 133 | padding: 10px 10px 10px 10px; 134 | font-weight: bold; 135 | color: coral; 136 | } 137 | -------------------------------------------------------------------------------- /examples/react-router-4/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom' 3 | import { connect } from 'react-redux' 4 | import styles from './App.css' 5 | import { logout } from '../actions/user' 6 | import { userIsAuthenticatedRedir, userIsNotAuthenticatedRedir, userIsAdminRedir, 7 | userIsAuthenticated, userIsNotAuthenticated } from '../auth' 8 | 9 | import AdminComponent from './Admin' 10 | import ProtectedComponent from './Protected' 11 | import LoginComponent from './Login' 12 | import Home from './Home' 13 | 14 | const getUserName = user => { 15 | if (user.data) { 16 | return `Welcome ${user.data.name}` 17 | } 18 | return `Not logged in` 19 | } 20 | 21 | // Need to apply the hocs here to avoid applying them inside the render method 22 | const Login = userIsNotAuthenticatedRedir(LoginComponent) 23 | const Protected = userIsAuthenticatedRedir(ProtectedComponent) 24 | const Admin = userIsAuthenticatedRedir(userIsAdminRedir(AdminComponent)) 25 | 26 | // Only show login when the user is not logged in and logout when logged in 27 | // Could have also done this with a single wrapper and `FailureComponent` 28 | const UserName = ({ user }) => (
{getUserName(user)}
) 29 | const LoginLink = userIsNotAuthenticated(() => Login) 30 | const LogoutLink = userIsAuthenticated(({ logout }) => logout()}>Logout) 31 | 32 | function App({ user, logout }) { 33 | return ( 34 | 35 |
36 | 41 | 46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | ) 55 | } 56 | 57 | const mapStateToProps = state => ({ 58 | user: state.user 59 | }) 60 | 61 | export default connect(mapStateToProps, { logout })(App) 62 | -------------------------------------------------------------------------------- /examples/react-router-4/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |

{"This demo serves as an example on how to use redux-auth-wrapper with react-router-4"}

7 |

{"Notice that Protected and Admin routes are protected and you will have to log in to see them."}

8 |

{"The Admin page required you to be logged in as an admin. Click Login to begin the demo."}

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /examples/react-router-4/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Loading() { 4 | return
Logging you in...
5 | } 6 | -------------------------------------------------------------------------------- /examples/react-router-4/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import styles from './App.css' 5 | import { login } from '../actions/user' 6 | 7 | export class LoginContainer extends Component { 8 | 9 | static propTypes = { 10 | login: PropTypes.func.isRequired 11 | }; 12 | 13 | onClick = (e) => { 14 | e.preventDefault() 15 | this.props.login({ 16 | name: this.refs.name.value, 17 | isAdmin: this.refs.admin.checked 18 | }) 19 | }; 20 | 21 | render() { 22 | return ( 23 |
24 |
25 | 26 |
27 |
28 | ) 29 | } 30 | 31 | } 32 | export default connect(null, { login })(LoginContainer) 33 | -------------------------------------------------------------------------------- /examples/react-router-4/components/Protected.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const Protected = ({ authData }) => { 5 | return ( 6 |
{`This is a protected page, you must be logged in if you are seeing this. Welcome ${authData.name}`}
7 | ) 8 | } 9 | export default connect(state => ({ authData: state.user.data }))(Protected) 10 | -------------------------------------------------------------------------------- /examples/react-router-4/constants.js: -------------------------------------------------------------------------------- 1 | export const USER_LOGGING_IN = 'USER_LOGGING_IN' 2 | export const USER_LOGGED_IN = 'USER_LOGGED_IN' 3 | export const USER_LOGGED_OUT = 'USER_LOGGED_OUT' 4 | -------------------------------------------------------------------------------- /examples/react-router-4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | redux-auth-wrapper loading rrv4 example 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/react-router-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raw-loading-rrv4-example", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "react-router": "5.1.2", 6 | "react-router-dom": "5.1.2", 7 | "redux-thunk": "2.3.0" 8 | }, 9 | "devDependencies": { 10 | "babel-loader": "7.1.5", 11 | "css-loader": "^0.28.4", 12 | "html-webpack-plugin": "3.2.0", 13 | "style-loader": "^0.18.2", 14 | "webpack": "4.41.5", 15 | "webpack-cli": "3.3.10", 16 | "webpack-dev-server": "3.10.1" 17 | }, 18 | "scripts": { 19 | "start": "webpack-dev-server --config webpack.config.babel.js --progress" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-router-4/reducers/index.js: -------------------------------------------------------------------------------- 1 | import user from './user' 2 | 3 | module.exports = { 4 | user 5 | } 6 | -------------------------------------------------------------------------------- /examples/react-router-4/reducers/user.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | const initialState = { 4 | data: null, 5 | isLoading: false 6 | } 7 | 8 | export default function userUpdate(state = initialState, { type, payload }) { 9 | switch (type) { 10 | case constants.USER_LOGGING_IN: 11 | return { ...initialState, isLoading: true } 12 | case constants.USER_LOGGED_IN: 13 | return { data: payload, isLoading: false } 14 | case constants.USER_LOGGED_OUT: 15 | return initialState 16 | default: 17 | return state 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-router-4/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | import HtmlWebpackPlugin from 'html-webpack-plugin' 4 | 5 | module.exports = { 6 | entry: './app.js', 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | devServer: { 12 | inline: true, 13 | historyApiFallback: true, 14 | stats: { 15 | colors: true, 16 | hash: false, 17 | version: false, 18 | chunks: false, 19 | children: false 20 | } 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | loaders: [ 'babel-loader' ], 27 | exclude: /node_modules/, 28 | include: __dirname 29 | }, 30 | { 31 | test: /\.css$/, 32 | loader: 'style-loader!css-loader?module=true&importLoaders=1&localIdentName=[name]_[local]_[hash:base64:5]', 33 | exclude: /semantic/ 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new HtmlWebpackPlugin({ 39 | template: 'index.html', // Load a custom template 40 | inject: 'body' // Inject all scripts into the body 41 | }) 42 | ] 43 | } 44 | 45 | // This will make the redux-auth-wrapper module resolve to the 46 | // latest src instead of using it from npm. Remove this if running 47 | // outside of the source. 48 | const lib = path.join(__dirname, '..', '..', 'lib') 49 | if (fs.existsSync(lib)) { 50 | // Use the latest src 51 | module.exports.resolve = { alias: { 'redux-auth-wrapper': lib } } 52 | // module.exports.module.loaders.push({ 53 | // test: /\.js$/, 54 | // loaders: [ 'babel' ], 55 | // include: lib 56 | // }) 57 | } else { 58 | throw "redux-auth-wrapper source not built. Run the following: 'pushd ../.. && rm -rf node_modules && yarn install && yarn run build && popd' and then rerun 'yarn start'" 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-auth-wrapper", 3 | "version": "3.0.0", 4 | "main": "index.js", 5 | "description": "A utility library for handling authentication and authorization for redux and react-router", 6 | "scripts": { 7 | "build": "mkdirp lib && babel ./src --out-dir ./lib", 8 | "build:clean": "rimraf ./lib", 9 | "build:copyFiles": "cp -rf package.json LICENSE.txt README.md lib/.", 10 | "dist": "cd lib && yarn publish", 11 | "dist:prepare": "yarn run build:clean && yarn run build && yarn run build:copyFiles", 12 | "lint": "eslint src test examples", 13 | "test": "mocha --compilers js:babel-core/register --recursive --require test/init.js test/authWrapper-test.js", 14 | "test:cov": "babel-node --max-old-space-size=4076 $(yarn bin)/babel-istanbul cover $(yarn bin)/_mocha -- --require test/init.js test/authWrapper-test.js", 15 | "test:watch": "mocha --compilers js:babel-core/register --recursive --require test/init.js -w test/authWrapper-test.js", 16 | "docs:clean": "rimraf _book", 17 | "docs:prepare": "gitbook install", 18 | "docs:build": "yarn run docs:prepare && gitbook build", 19 | "docs:watch": "yarn run docs:prepare && gitbook serve", 20 | "docs:publish": "yarn run docs:clean && yarn run docs:build && cp README.md _book && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:mjrussell/redux-auth-wrapper gh-pages --force" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/mjrussell/redux-auth-wrapper.git" 25 | }, 26 | "authors": [ 27 | "Matthew Russell" 28 | ], 29 | "license": "MIT", 30 | "devDependencies": { 31 | "babel-cli": "6.26.0", 32 | "babel-core": "6.26.3", 33 | "babel-eslint": "10.0.3", 34 | "babel-istanbul": "0.12.2", 35 | "babel-plugin-transform-decorators-legacy": "1.3.5", 36 | "babel-polyfill": "6.26.0", 37 | "babel-preset-es2015": "6.24.1", 38 | "babel-preset-react": "6.24.1", 39 | "babel-preset-stage-0": "6.24.1", 40 | "chai": "4.2.0", 41 | "coveralls": "3.0.9", 42 | "enzyme": "3.11.0", 43 | "enzyme-adapter-react-16": "1.15.2", 44 | "eslint": "6.8.0", 45 | "eslint-plugin-react": "7.17.0", 46 | "expect": "24.9.0", 47 | "gitbook-cli": "2.3.2", 48 | "jsdom": "15.2.1", 49 | "lodash": "4.17.21", 50 | "mkdirp": "0.5.1", 51 | "mocha": "6.2.2", 52 | "prop-types": "^15.7.2", 53 | "react": "16.12.0", 54 | "react-dom": "16.12.0", 55 | "react-redux": "7.1.3", 56 | "redux": "4.0.5", 57 | "rimraf": "3.0.0", 58 | "sinon": "8.0.2" 59 | }, 60 | "dependencies": { 61 | "hoist-non-react-statics": "^3.3.0", 62 | "invariant": "^2.2.4", 63 | "lodash.isempty": "^4.4.0", 64 | "query-string": "^6.9.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /runTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf node_modules/react-router* 4 | rm -rf node_modules/history 5 | 6 | if [ "$REACT_ROUTER_VERSION" = "3" ]; then 7 | yarn add react-router@3.2.5 8 | yarn add react-router-redux@4.0.8 9 | yarn run test:cov test/rrv3-test.js 10 | elif [ "$REACT_ROUTER_VERSION" = "4" ]; then 11 | yarn add react-router@5.1.2 12 | yarn add history@4.7.2 13 | yarn add connected-react-router@6.6.1 14 | yarn run test:cov test/rrv4-test.js 15 | fi 16 | -------------------------------------------------------------------------------- /src/authWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import hoistStatics from 'hoist-non-react-statics' 4 | 5 | const defaults = { 6 | AuthenticatingComponent: () => null, // dont render anything while authenticating 7 | FailureComponent: () => null, // dont render anything on failure of the predicate 8 | wrapperDisplayName: 'AuthWrapper' 9 | } 10 | 11 | export default (args) => { 12 | const { AuthenticatingComponent, FailureComponent, wrapperDisplayName } = { 13 | ...defaults, 14 | ...args 15 | } 16 | 17 | // Wraps the component that needs the auth enforcement 18 | function wrapComponent(DecoratedComponent) { 19 | const displayName = DecoratedComponent.displayName || DecoratedComponent.name || 'Component' 20 | 21 | class UserAuthWrapper extends Component { 22 | 23 | static displayName = `${wrapperDisplayName}(${displayName})`; 24 | 25 | static propTypes = { 26 | isAuthenticated: PropTypes.bool, 27 | isAuthenticating: PropTypes.bool 28 | }; 29 | 30 | static defaultProps = { 31 | isAuthenticating: false 32 | } 33 | 34 | render() { 35 | const { isAuthenticated, isAuthenticating } = this.props 36 | if (isAuthenticated) { 37 | return 38 | } else if(isAuthenticating) { 39 | return 40 | } else { 41 | return 42 | } 43 | } 44 | } 45 | 46 | return hoistStatics(UserAuthWrapper, DecoratedComponent) 47 | } 48 | 49 | return wrapComponent 50 | } 51 | -------------------------------------------------------------------------------- /src/connectedAuthWrapper.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | 3 | import authWrapper from './authWrapper' 4 | 5 | const connectedDefaults = { 6 | authenticatingSelector: () => false 7 | } 8 | 9 | export default (args) => { 10 | const { authenticatedSelector, authenticatingSelector } = { 11 | ...connectedDefaults, 12 | ...args 13 | } 14 | 15 | return (DecoratedComponent) => 16 | connect((state, ownProps) => ({ 17 | isAuthenticated: authenticatedSelector(state, ownProps), 18 | isAuthenticating: authenticatingSelector(state, ownProps) 19 | }))(authWrapper(args)(DecoratedComponent)) 20 | } 21 | -------------------------------------------------------------------------------- /src/helper/redirect.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import invariant from 'invariant' 3 | 4 | import authWrapper from '../authWrapper' 5 | import Redirect from '../redirect' 6 | 7 | const connectedDefaults = { 8 | authenticatingSelector: () => false, 9 | allowRedirectBack: true, 10 | FailureComponent: Redirect, 11 | redirectQueryParamName: 'redirect' 12 | } 13 | 14 | export default ({ locationHelperBuilder, getRouterRedirect }) => { 15 | 16 | const connectedRouterRedirect = (args) => { 17 | const allArgs = { ...connectedDefaults, ...args } 18 | const { FailureComponent, redirectPath, authenticatedSelector, authenticatingSelector, allowRedirectBack, redirectQueryParamName } = allArgs 19 | 20 | const { createRedirectLoc } = locationHelperBuilder({ 21 | redirectQueryParamName 22 | }) 23 | 24 | let redirectPathSelector 25 | if (typeof redirectPath === 'string') { 26 | redirectPathSelector = () => redirectPath 27 | } else if (typeof redirectPath === 'function') { 28 | redirectPathSelector = redirectPath 29 | } else { 30 | invariant(false, 'redirectPath must be either a string or a function') 31 | } 32 | 33 | let allowRedirectBackFn 34 | if (typeof allowRedirectBack === 'boolean') { 35 | allowRedirectBackFn = () => allowRedirectBack 36 | } else if (typeof allowRedirectBack === 'function') { 37 | allowRedirectBackFn = allowRedirectBack 38 | } else { 39 | invariant(false, 'allowRedirectBack must be either a boolean or a function') 40 | } 41 | 42 | const redirect = (replace) => (props, path) => 43 | replace(createRedirectLoc(allowRedirectBackFn(props, path))(props, path)) 44 | 45 | const ConnectedFailureComponent = connect((state, ownProps) => ({ 46 | redirect: redirect(getRouterRedirect(ownProps)) 47 | }))(FailureComponent) 48 | 49 | return (DecoratedComponent) => 50 | connect((state, ownProps) => ({ 51 | redirectPath: redirectPathSelector(state, ownProps), 52 | isAuthenticated: authenticatedSelector(state, ownProps), 53 | isAuthenticating: authenticatingSelector(state, ownProps) 54 | }))(authWrapper({ ...allArgs, FailureComponent: ConnectedFailureComponent })(DecoratedComponent)) 55 | } 56 | 57 | const connectedReduxRedirect = (args) => { 58 | const allArgs = { ...connectedDefaults, ...args } 59 | const { FailureComponent, redirectPath, authenticatedSelector, authenticatingSelector, allowRedirectBack, redirectAction, redirectQueryParamName } = allArgs 60 | 61 | const { createRedirectLoc } = locationHelperBuilder({ 62 | redirectQueryParamName 63 | }) 64 | 65 | let redirectPathSelector 66 | if (typeof redirectPath === 'string') { 67 | redirectPathSelector = () => redirectPath 68 | } else if (typeof redirectPath === 'function') { 69 | redirectPathSelector = redirectPath 70 | } else { 71 | invariant(false, 'redirectPath must be either a string or a function') 72 | } 73 | 74 | let allowRedirectBackFn 75 | if (typeof allowRedirectBack === 'boolean') { 76 | allowRedirectBackFn = () => allowRedirectBack 77 | } else if (typeof allowRedirectBack === 'function') { 78 | allowRedirectBackFn = allowRedirectBack 79 | } else { 80 | invariant(false, 'allowRedirectBack must be either a boolean or a function') 81 | } 82 | 83 | const createRedirect = (dispatch) => ({ 84 | redirect: (props, path) => dispatch(redirectAction(createRedirectLoc(allowRedirectBackFn(props, path))(props, path))) 85 | }) 86 | 87 | const ConnectedFailureComponent = connect(null, createRedirect)(FailureComponent) 88 | 89 | return (DecoratedComponent) => 90 | connect((state, ownProps) => ({ 91 | redirectPath: redirectPathSelector(state, ownProps), 92 | isAuthenticated: authenticatedSelector(state, ownProps), 93 | isAuthenticating: authenticatingSelector(state, ownProps) 94 | }))(authWrapper({ ...allArgs, FailureComponent: ConnectedFailureComponent })(DecoratedComponent)) 95 | } 96 | 97 | return { 98 | connectedRouterRedirect, 99 | connectedReduxRedirect 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/history3/locationHelper.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | 3 | const defaults = { 4 | redirectQueryParamName: 'redirect', 5 | locationSelector: ({ location }) => location 6 | } 7 | 8 | export default (args) => { 9 | const { redirectQueryParamName, locationSelector } = { 10 | ...defaults, 11 | ...args 12 | } 13 | 14 | const getRedirectQueryParam = (props) => { 15 | const location = locationSelector(props) 16 | return location.query[redirectQueryParamName] 17 | } 18 | 19 | const createRedirectLoc = allowRedirectBack => (props, redirectPath) => { 20 | const location = locationSelector(props) 21 | const redirectLoc = url.parse(redirectPath, true) 22 | 23 | let query 24 | 25 | if (allowRedirectBack) { 26 | query = { [redirectQueryParamName]: `${location.pathname}${location.search}${location.hash}` } 27 | } else { 28 | query = {} 29 | } 30 | 31 | query = { 32 | ...query, 33 | ...redirectLoc.query 34 | } 35 | 36 | return { 37 | pathname: redirectLoc.pathname, 38 | hash: redirectLoc.hash, 39 | query 40 | } 41 | } 42 | 43 | return { 44 | getRedirectQueryParam, 45 | createRedirectLoc 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/history3/redirect.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from '../history3/locationHelper' 2 | import redirectUtil from '../helper/redirect' 3 | import invariant from 'invariant' 4 | 5 | export const { connectedRouterRedirect, connectedReduxRedirect } = redirectUtil({ 6 | locationHelperBuilder, 7 | getRouterRedirect: ({ router }) => router.replace 8 | }) 9 | 10 | const onEnterDefaults = { 11 | allowRedirectBack: true, 12 | authenticatingSelector: () => false, 13 | redirectQueryParamName: 'redirect' 14 | } 15 | 16 | export const createOnEnter = (config) => { 17 | const { authenticatedSelector, authenticatingSelector, redirectPath, allowRedirectBack, redirectQueryParamName } = { 18 | ...onEnterDefaults, 19 | ...config 20 | } 21 | 22 | let redirectPathSelector 23 | if (typeof redirectPath === 'string') { 24 | redirectPathSelector = () => redirectPath 25 | } else if (typeof redirectPath === 'function') { 26 | redirectPathSelector = redirectPath 27 | } else { 28 | invariant(false, 'redirectPath must be either a string or a function') 29 | } 30 | 31 | let allowRedirectBackFn 32 | if (typeof allowRedirectBack === 'boolean') { 33 | allowRedirectBackFn = () => allowRedirectBack 34 | } else if (typeof allowRedirectBack === 'function') { 35 | allowRedirectBackFn = allowRedirectBack 36 | } else { 37 | invariant(false, 'allowRedirectBack must be either a boolean or a function') 38 | } 39 | 40 | return (store, nextState, replace) => { 41 | 42 | const { createRedirectLoc } = locationHelperBuilder({ 43 | redirectQueryParamName 44 | }) 45 | 46 | const isAuthenticated = authenticatedSelector(store.getState(), nextState) 47 | const isAuthenticating = authenticatingSelector(store.getState(), nextState) 48 | 49 | if (!isAuthenticated && !isAuthenticating) { 50 | const redirectPath = redirectPathSelector(store.getState(), nextState) 51 | replace(createRedirectLoc(allowRedirectBackFn(nextState, redirectPath))(nextState, redirectPath)) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/history4/locationHelper.js: -------------------------------------------------------------------------------- 1 | import url from 'url' 2 | import { stringify, parse } from 'query-string' 3 | 4 | const defaults = { 5 | redirectQueryParamName: 'redirect', 6 | locationSelector: ({ location }) => location 7 | } 8 | 9 | export default (args) => { 10 | const { redirectQueryParamName, locationSelector } = { 11 | ...defaults, 12 | ...args 13 | } 14 | 15 | const getRedirectQueryParam = (props) => { 16 | const location = locationSelector(props) 17 | const query = parse(location.search) 18 | return query[redirectQueryParamName] 19 | } 20 | 21 | const createRedirectLoc = allowRedirectBack => (props, redirectPath) => { 22 | const location = locationSelector(props) 23 | const redirectLoc = url.parse(redirectPath, true) 24 | 25 | let query 26 | 27 | if (allowRedirectBack) { 28 | query = { [redirectQueryParamName]: `${location.pathname}${location.search}${location.hash}` } 29 | } else { 30 | query = {} 31 | } 32 | 33 | query = { 34 | ...query, 35 | ...redirectLoc.query 36 | } 37 | 38 | return { 39 | pathname: redirectLoc.pathname, 40 | hash: redirectLoc.hash, 41 | search: stringify(query) 42 | } 43 | } 44 | 45 | return { 46 | getRedirectQueryParam, 47 | createRedirectLoc 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/history4/redirect.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from '../history4/locationHelper' 2 | import redirectUtil from '../helper/redirect' 3 | 4 | export const { connectedRouterRedirect, connectedReduxRedirect } = redirectUtil({ 5 | locationHelperBuilder, 6 | getRouterRedirect: ({ history }) => history.replace 7 | }) 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjrussell/redux-auth-wrapper/401f155f754aaaf8b139958468671f6c4a4013c0/src/index.js -------------------------------------------------------------------------------- /src/redirect.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class Redirect extends Component { 5 | 6 | static propTypes = { 7 | redirectPath: PropTypes.string.isRequired, 8 | redirect: PropTypes.func.isRequired 9 | }; 10 | 11 | UNSAFE_componentWillMount() { 12 | this.props.redirect(this.props, this.props.redirectPath) 13 | } 14 | 15 | render() { 16 | // Redirect should happen before this is rendered 17 | return null 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/authWrapper-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha, jasmine */ 2 | import React, { Component } from 'react' 3 | import _ from 'lodash' 4 | import { createStore, combineReducers } from 'redux' 5 | import { Provider } from 'react-redux' 6 | import { mount } from 'enzyme' 7 | 8 | import { userLoggedOut, userLoggedIn, userLoggingIn, authenticatedSelector, authenticatingSelector, userReducer, 9 | UnprotectedComponent, FailureComponent, AuthenticatingComponent, userDataSelector } from './helpers' 10 | 11 | import authWrapper from '../src/authWrapper' 12 | import connectedAuthWrapper from '../src/connectedAuthWrapper' 13 | 14 | const defaultConfig = { 15 | authenticatedSelector 16 | } 17 | 18 | describe('connectedAuthWrapper', () => { 19 | it('renders the wrapped component on success and hides on failure', () => { 20 | const auth = connectedAuthWrapper(defaultConfig) 21 | 22 | const rootReducer = combineReducers({ user: userReducer }) 23 | const store = createStore(rootReducer) 24 | 25 | const AuthedComponent = auth(UnprotectedComponent) 26 | 27 | const wrapper = mount( 28 | 29 | 30 | 31 | ) 32 | 33 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 34 | expect(wrapper.find(AuthedComponent).html()).to.equal('') 35 | 36 | store.dispatch(userLoggedIn()) 37 | wrapper.update() 38 | 39 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 40 | }) 41 | 42 | it('defaults to rendering nothing while authenticating', () => { 43 | const auth = connectedAuthWrapper({ 44 | ...defaultConfig, 45 | authenticatingSelector 46 | }) 47 | 48 | const rootReducer = combineReducers({ user: userReducer }) 49 | const store = createStore(rootReducer) 50 | 51 | const AuthedComponent = auth(UnprotectedComponent) 52 | 53 | const wrapper = mount( 54 | 55 | 56 | 57 | ) 58 | 59 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 60 | expect(wrapper.find(AuthedComponent).html()).to.equal('') 61 | 62 | store.dispatch(userLoggingIn()) 63 | wrapper.update() 64 | 65 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 66 | expect(wrapper.find(AuthedComponent).html()).to.equal('') 67 | 68 | store.dispatch(userLoggedIn()) 69 | wrapper.update() 70 | 71 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 72 | }) 73 | 74 | it('renders the specified component on failure', () => { 75 | const auth = connectedAuthWrapper({ 76 | ...defaultConfig, 77 | FailureComponent 78 | }) 79 | 80 | const rootReducer = combineReducers({ user: userReducer }) 81 | const store = createStore(rootReducer) 82 | 83 | const AuthedComponent = auth(UnprotectedComponent) 84 | 85 | const wrapper = mount( 86 | 87 | 88 | 89 | ) 90 | 91 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 92 | expect(wrapper.find(FailureComponent).length).to.equal(1) 93 | 94 | store.dispatch(userLoggedIn()) 95 | wrapper.update() 96 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 97 | 98 | store.dispatch(userLoggedOut()) 99 | wrapper.update() 100 | 101 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 102 | expect(wrapper.find(FailureComponent).length).to.equal(1) 103 | }) 104 | 105 | it('supports a custom authenticated function', () => { 106 | const auth = connectedAuthWrapper({ 107 | ...defaultConfig, 108 | authenticatedSelector: state => userDataSelector(state).firstName === 'Matt' 109 | }) 110 | 111 | const rootReducer = combineReducers({ user: userReducer }) 112 | const store = createStore(rootReducer) 113 | 114 | const AuthedComponent = auth(UnprotectedComponent) 115 | 116 | const wrapper = mount( 117 | 118 | 119 | 120 | ) 121 | 122 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 123 | 124 | store.dispatch(userLoggedIn()) 125 | wrapper.update() 126 | 127 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 128 | 129 | store.dispatch(userLoggedOut()) 130 | wrapper.update() 131 | 132 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 133 | 134 | store.dispatch(userLoggedIn('Matt')) 135 | wrapper.update() 136 | 137 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 138 | }) 139 | 140 | it('Allows for custom wrapper display name', () => { 141 | const auth = connectedAuthWrapper({ 142 | ...defaultConfig, 143 | wrapperDisplayName: 'Better Name' 144 | }) 145 | 146 | const rootReducer = combineReducers({ user: userReducer }) 147 | const store = createStore(rootReducer) 148 | 149 | const AuthedComponent = auth(UnprotectedComponent) 150 | 151 | const wrapper = mount( 152 | 153 | 154 | 155 | ) 156 | 157 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 158 | expect(wrapper.find(AuthedComponent).html()).to.equal('') 159 | 160 | store.dispatch(userLoggedIn()) 161 | wrapper.update() 162 | 163 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 164 | expect(wrapper.find(AuthedComponent).childAt(0).name()).to.equal('Better Name(UnprotectedComponent)') 165 | }) 166 | 167 | it('Display name works for name-less components', () => { 168 | const auth = connectedAuthWrapper(defaultConfig) 169 | 170 | const rootReducer = combineReducers({ user: userReducer }) 171 | const store = createStore(rootReducer) 172 | 173 | const AuthedComponent = auth(() => null) 174 | 175 | const wrapper = mount( 176 | 177 | 178 | 179 | ) 180 | 181 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 182 | expect(wrapper.find(AuthedComponent).html()).to.equal('') 183 | 184 | store.dispatch(userLoggedIn()) 185 | wrapper.update() 186 | 187 | expect(wrapper.find(AuthedComponent).childAt(0).name()).to.equal('AuthWrapper(Component)') 188 | }) 189 | 190 | it('passes through props to components', () => { 191 | const auth = connectedAuthWrapper({ 192 | ...defaultConfig, 193 | authenticatingSelector, 194 | AuthenticatingComponent, 195 | FailureComponent 196 | }) 197 | 198 | const rootReducer = combineReducers({ user: userReducer }) 199 | const store = createStore(rootReducer) 200 | 201 | const AuthedComponent = auth(UnprotectedComponent) 202 | 203 | const wrapper = mount( 204 | 205 | 206 | 207 | ) 208 | 209 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 210 | expect(_.omit(wrapper.find(FailureComponent).props(), [ 'dispatch' ])).to.deep.equal({ 211 | isAuthenticated: false, isAuthenticating: false, testProp: 'test' 212 | }) 213 | 214 | store.dispatch(userLoggingIn()) 215 | wrapper.update() 216 | 217 | expect(wrapper.find(UnprotectedComponent).length).to.equal(0) 218 | expect(_.omit(wrapper.find(AuthenticatingComponent).props(), [ 'dispatch' ])).to.deep.equal({ 219 | isAuthenticated: false, isAuthenticating: true, testProp: 'test' 220 | }) 221 | 222 | store.dispatch(userLoggedIn()) 223 | wrapper.update() 224 | 225 | expect(wrapper.find(UnprotectedComponent).length).to.equal(1) 226 | expect(_.omit(wrapper.find(UnprotectedComponent).props(), [ 'dispatch' ])).to.deep.equal({ 227 | isAuthenticated: true, isAuthenticating: false, testProp: 'test' 228 | }) 229 | }) 230 | 231 | it('hoists statics to the wrapper', () => { 232 | const auth = authWrapper(defaultConfig) 233 | 234 | class WithStatic extends Component { 235 | static staticProp = true; 236 | 237 | render() { 238 | return
239 | } 240 | } 241 | 242 | WithStatic.staticFun = () => 'auth' 243 | 244 | const authed = auth(WithStatic) 245 | expect(authed.staticProp).to.equal(true) 246 | expect(authed.staticFun).to.be.a('function') 247 | expect(authed.staticFun()).to.equal('auth') 248 | }) 249 | }) 250 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import _ from 'lodash' 4 | 5 | export const USER_LOGGED_IN = 'USER_LOGGED_IN' 6 | export const USER_LOGGED_OUT = 'USER_LOGGED_OUT' 7 | export const USER_LOGGING_IN = 'USER_LOGGING_IN' 8 | 9 | export const userReducerInitialState = { 10 | userData: {}, 11 | isAuthenticating: false 12 | } 13 | 14 | export const userReducer = (state = userReducerInitialState, { type, payload }) => { 15 | if (type === USER_LOGGED_IN) { 16 | return { 17 | userData: payload, 18 | isAuthenticating: false 19 | } 20 | } else if (type === USER_LOGGED_OUT) { 21 | return userReducerInitialState 22 | } else if (type === USER_LOGGING_IN) { 23 | return { 24 | ...state, 25 | isAuthenticating: true 26 | } 27 | } 28 | return state 29 | } 30 | 31 | export const userDataSelector = state => state.user.userData 32 | 33 | export const authenticatedSelector = state => !_.isEmpty(userDataSelector(state)) 34 | 35 | export const authenticatingSelector = state => state.user.isAuthenticating 36 | 37 | export const userLoggedIn = (firstName = 'Test', lastName = 'McDuderson') => ({ 38 | type: USER_LOGGED_IN, 39 | payload: { 40 | email: 'test@test.com', 41 | firstName, 42 | lastName 43 | } 44 | }) 45 | 46 | export const userLoggedOut = () => ({ 47 | type: USER_LOGGED_OUT 48 | }) 49 | 50 | export const userLoggingIn = () => ({ 51 | type: USER_LOGGING_IN 52 | }) 53 | 54 | export const defaultConfig = { 55 | redirectPath: '/login', 56 | authenticatedSelector, 57 | authenticatingSelector 58 | } 59 | 60 | export class AuthenticatingComponent extends Component { 61 | render() { 62 | return ( 63 |
Loading!
64 | ) 65 | } 66 | } 67 | 68 | export function FailureComponent(props) { 69 | if (props.authData) { 70 | return
No Access for user: {props.authData.email}
71 | } else { 72 | return
No Access
73 | } 74 | } 75 | 76 | export class UnprotectedComponent extends Component { 77 | render() { 78 | return ( 79 |
80 | ) 81 | } 82 | } 83 | 84 | export class UnprotectedParentComponent extends Component { 85 | static propTypes = { 86 | children: PropTypes.node 87 | }; 88 | 89 | render() { 90 | return ( 91 |
92 | {this.props.children} 93 |
94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import jsdom from 'jsdom' 3 | import Enzyme from 'enzyme' 4 | import Adapter from 'enzyme-adapter-react-16' 5 | 6 | Enzyme.configure({ adapter: new Adapter() }) 7 | 8 | // Use except 9 | global.expect = chai.expect 10 | 11 | // JsDom browser 12 | const { JSDOM } = jsdom; 13 | 14 | const { document } = (new JSDOM('')).window; 15 | global.document = document; 16 | global.window = document.defaultView 17 | global.navigator = global.window.navigator 18 | -------------------------------------------------------------------------------- /test/redirectBase-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha, jasmine */ 2 | import React, { Component } from 'react' 3 | import sinon from 'sinon' 4 | import _ from 'lodash' 5 | 6 | import Redirect from '../src/redirect' 7 | import { userLoggedIn, userLoggedOut, userLoggingIn, userDataSelector, authenticatedSelector, defaultConfig, 8 | UnprotectedComponent, AuthenticatingComponent, FailureComponent } from './helpers' 9 | 10 | export default (setupTest, versionName, getRouteParams, getQueryParams, getRedirectQueryParam, authWrapper) => { 11 | 12 | describe(`wrapper ${versionName} Base`, () => { 13 | 14 | it('redirects unauthenticated', () => { 15 | const auth = authWrapper(defaultConfig) 16 | const routes = [ 17 | { path: '/login', component: UnprotectedComponent }, 18 | { path: '/auth', component: auth(UnprotectedComponent) } 19 | ] 20 | 21 | const { history, getLocation, wrapper } = setupTest(routes) 22 | 23 | expect(getLocation().pathname).to.equal('/') 24 | expect(getQueryParams(getLocation())).to.be.empty 25 | history.push('/auth') 26 | wrapper.update() 27 | 28 | expect(getLocation().pathname).to.equal('/login') 29 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 30 | }) 31 | 32 | it('does not redirect if authenticating', () => { 33 | const auth = authWrapper({ 34 | ...defaultConfig, 35 | authenticatingSelector: () => true, 36 | AuthenticatingComponent: AuthenticatingComponent 37 | }) 38 | const routes = [ 39 | { path: 'auth', component: auth(UnprotectedComponent) } 40 | ] 41 | 42 | const { history, getLocation, wrapper } = setupTest(routes) 43 | 44 | expect(getLocation().pathname).to.equal('/') 45 | expect(getQueryParams(getLocation())).to.be.empty 46 | history.push('/auth') 47 | wrapper.update() 48 | 49 | expect(getLocation().pathname).to.equal('/auth') 50 | expect(getQueryParams(getLocation())).to.be.empty 51 | }) 52 | 53 | it('by default ignores authenticating', () => { 54 | const auth = authWrapper({ authenticatedSelector, redirectPath: '/login' }) 55 | 56 | const routes = [ 57 | { path: '/login', component: UnprotectedComponent }, 58 | { path: '/auth', component: auth(UnprotectedComponent) } 59 | ] 60 | 61 | const { history, getLocation, wrapper } = setupTest(routes) 62 | 63 | expect(getLocation().pathname).to.equal('/') 64 | expect(getQueryParams(getLocation())).to.be.empty 65 | history.push('/auth') 66 | wrapper.update() 67 | 68 | expect(getLocation().pathname).to.equal('/login') 69 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 70 | }) 71 | 72 | it('renders the specified component when authenticating', () => { 73 | const auth = authWrapper({ 74 | ...defaultConfig, 75 | authenticatingSelector: () => true, 76 | AuthenticatingComponent: AuthenticatingComponent 77 | }) 78 | const routes = [ 79 | { path: '/auth', component: auth(UnprotectedComponent) } 80 | ] 81 | 82 | const { wrapper, history } = setupTest(routes) 83 | 84 | history.push('/auth') 85 | wrapper.update() 86 | 87 | const comp = wrapper.find(AuthenticatingComponent) 88 | // Props from React-Router 89 | expect(comp.props().location.pathname).to.equal('/auth') 90 | }) 91 | 92 | it('renders nothing while authenticating and no AuthenticatingComponent set', () => { 93 | const auth = authWrapper({ 94 | ...defaultConfig, 95 | authenticatingSelector: () => true 96 | }) 97 | const routes = [ 98 | { path: '/auth', component: auth(UnprotectedComponent) } 99 | ] 100 | 101 | const { wrapper, history } = setupTest(routes) 102 | 103 | history.push('/auth') 104 | wrapper.update() 105 | 106 | const comp = wrapper.find('#testRoot') 107 | // There is a child here. It is connect but it should render no html 108 | expect(comp.childAt(0).html()).to.equal('') 109 | }) 110 | 111 | it('renders the failure component when prop is set', () => { 112 | const auth = authWrapper({ 113 | ...defaultConfig, 114 | authenticatedSelector: () => false, 115 | FailureComponent 116 | }) 117 | const routes = [ 118 | { path: '/auth', component: auth(UnprotectedComponent) } 119 | ] 120 | 121 | const { history, store, wrapper } = setupTest(routes) 122 | 123 | store.dispatch(userLoggedIn()) 124 | 125 | history.push('/auth') 126 | wrapper.update() 127 | 128 | const comp = wrapper.find(FailureComponent).last() 129 | expect(comp.props().isAuthenticated).to.be.false 130 | }) 131 | 132 | it('preserves query params on redirect', () => { 133 | const auth = authWrapper(defaultConfig) 134 | const routes = [ 135 | { path: '/login', component: UnprotectedComponent }, 136 | { path: '/auth', component: auth(UnprotectedComponent) } 137 | ] 138 | 139 | const { history, getLocation, wrapper } = setupTest(routes) 140 | 141 | expect(getLocation().pathname).to.equal('/') 142 | expect(getQueryParams(getLocation())).to.be.empty 143 | history.push('/auth?test=foo') 144 | wrapper.update() 145 | 146 | expect(getLocation().pathname).to.equal('/login') 147 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth?test=foo' }) 148 | }) 149 | 150 | it('allows authenticated users', () => { 151 | const auth = authWrapper(defaultConfig) 152 | const routes = [ 153 | { path: '/login', component: UnprotectedComponent }, 154 | { path: '/auth', component: auth(UnprotectedComponent) } 155 | ] 156 | 157 | const { store, history, getLocation, wrapper } = setupTest(routes) 158 | 159 | store.dispatch(userLoggedIn()) 160 | 161 | history.push('/auth') 162 | wrapper.update() 163 | 164 | expect(getLocation().pathname).to.equal('/auth') 165 | }) 166 | 167 | it('redirects on no longer authorized', () => { 168 | const auth = authWrapper(defaultConfig) 169 | const routes = [ 170 | { path: '/login', component: UnprotectedComponent }, 171 | { path: '/auth', component: auth(UnprotectedComponent) } 172 | ] 173 | 174 | const { store, history, getLocation, wrapper } = setupTest(routes) 175 | 176 | store.dispatch(userLoggedIn()) 177 | 178 | history.push('/auth') 179 | wrapper.update() 180 | expect(getLocation().pathname).to.equal('/auth') 181 | 182 | store.dispatch(userLoggedOut()) 183 | wrapper.update() 184 | expect(getLocation().pathname).to.equal('/login') 185 | }) 186 | 187 | it('redirects if no longer authenticating', () => { 188 | const auth = authWrapper(defaultConfig) 189 | const routes = [ 190 | { path: '/login', component: UnprotectedComponent }, 191 | { path: '/auth', component: auth(UnprotectedComponent) } 192 | ] 193 | 194 | const { store, history, getLocation, wrapper } = setupTest(routes) 195 | 196 | store.dispatch(userLoggingIn()) 197 | 198 | history.push('/auth') 199 | wrapper.update() 200 | expect(getLocation().pathname).to.equal('/auth') 201 | 202 | store.dispatch(userLoggedOut()) 203 | wrapper.update() 204 | expect(getLocation().pathname).to.equal('/login') 205 | }) 206 | 207 | it('optionally prevents redirection', () => { 208 | const auth = authWrapper({ 209 | ...defaultConfig, 210 | authenticatedSelector: () => false, 211 | allowRedirectBack: false 212 | }) 213 | const routes = [ 214 | { path: '/login', component: UnprotectedComponent }, 215 | { path: '/auth', component: auth(UnprotectedComponent) } 216 | ] 217 | 218 | const { history, store, getLocation, wrapper } = setupTest(routes) 219 | 220 | store.dispatch(userLoggedIn()) 221 | wrapper.update() 222 | 223 | history.push('/auth') 224 | wrapper.update() 225 | expect(getLocation().pathname).to.equal('/login') 226 | expect(getQueryParams(getLocation())).to.deep.equal({}) 227 | }) 228 | 229 | it('optionally prevents redirection from a function result', () => { 230 | const auth = authWrapper({ 231 | ...defaultConfig, 232 | authenticatedSelector: () => false, 233 | allowRedirectBack: ({ location }, redirectPath) => location.pathname === '/auth' && redirectPath === '/login' 234 | }) 235 | const routes = [ 236 | { path: '/login', component: UnprotectedComponent }, 237 | { path: '/auth', component: auth(UnprotectedComponent) }, 238 | { path: '/authNoRedir', component: auth(UnprotectedComponent) } 239 | ] 240 | 241 | const { history, store, getLocation, wrapper } = setupTest(routes) 242 | 243 | store.dispatch(userLoggedIn()) 244 | 245 | history.push('/auth') 246 | wrapper.update() 247 | expect(getLocation().pathname).to.equal('/login') 248 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 249 | 250 | history.push('/authNoRedir') 251 | wrapper.update() 252 | expect(getLocation().pathname).to.equal('/login') 253 | expect(getQueryParams(getLocation())).to.deep.equal({}) 254 | }) 255 | 256 | it('can be nested', () => { 257 | const firstNameAuth = authWrapper({ 258 | ...defaultConfig, 259 | authenticatedSelector: (state) => userDataSelector(state).firstName === 'Test' 260 | }) 261 | 262 | const lastNameAuth = authWrapper({ 263 | ...defaultConfig, 264 | authenticatedSelector: (state) => userDataSelector(state).lastName === 'McDuderson' 265 | }) 266 | 267 | const routes = [ 268 | { path: '/login', component: UnprotectedComponent }, 269 | { path: '/auth', component: firstNameAuth(lastNameAuth(UnprotectedComponent)) } 270 | ] 271 | 272 | const { history, store, getLocation, wrapper } = setupTest(routes) 273 | 274 | store.dispatch(userLoggedIn('NotTest')) 275 | wrapper.update() 276 | 277 | history.push('/auth') 278 | wrapper.update() 279 | expect(getLocation().pathname).to.equal('/login') 280 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 281 | 282 | store.dispatch(userLoggedIn('Test', 'NotMcDuderson')) 283 | wrapper.update() 284 | 285 | history.push('/auth') 286 | wrapper.update() 287 | expect(getLocation().pathname).to.equal('/login') 288 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 289 | 290 | store.dispatch(userLoggedIn()) 291 | wrapper.update() 292 | 293 | history.push('/auth') 294 | wrapper.update() 295 | expect(getLocation().pathname).to.equal('/auth') 296 | expect(getQueryParams(getLocation())).to.be.empty 297 | }) 298 | 299 | it('passes props to authed components', () => { 300 | const auth = authWrapper(defaultConfig) 301 | const Child = auth(UnprotectedComponent) 302 | 303 | class PropParentComponent extends Component { 304 | 305 | render() { 306 | // Need to pass down at least location from router, but can just pass it all down 307 | return 308 | } 309 | } 310 | 311 | const routes = [ 312 | { path: '/prop', component: PropParentComponent } 313 | ] 314 | const { history, store, wrapper } = setupTest(routes) 315 | 316 | store.dispatch(userLoggedIn()) 317 | wrapper.update() 318 | 319 | history.push('/prop') 320 | wrapper.update() 321 | 322 | const comp = wrapper.find(UnprotectedComponent) 323 | // Props from React-Router 324 | expect(comp.props().location.pathname).to.equal('/prop') 325 | // Props from parent 326 | expect(comp.props().testProp).to.equal(true) 327 | // No extra wrapper props 328 | expect(Object.keys(_.omit(wrapper.find(UnprotectedComponent).props(), [ 329 | 'children', 'location', 'params', 'route', 'routeParams', 'router', 'routes', 'history', 'match', 'staticContext', 'dispatch' 330 | ])).sort()).to.deep.equal([ 331 | 'isAuthenticated', 'isAuthenticating', 'redirectPath', 'testProp' 332 | ]) 333 | }) 334 | 335 | it('passes ownProps for authenticated selector', () => { 336 | const auth = authWrapper({ 337 | ...defaultConfig, 338 | authenticatedSelector: (state, ownProps) => { 339 | const user = userDataSelector(state) 340 | const params = getRouteParams(ownProps) // from React-Router 341 | return user.firstName === 'Test' && params.id === '1' 342 | } 343 | }) 344 | 345 | const routes = [ 346 | { path: '/login', component: UnprotectedComponent }, 347 | { path: '/auth/:id', component: auth(UnprotectedComponent) } 348 | ] 349 | 350 | const { history, store, getLocation, wrapper } = setupTest(routes) 351 | 352 | expect(getLocation().pathname).to.equal('/') 353 | expect(getQueryParams(getLocation())).to.be.empty 354 | 355 | store.dispatch(userLoggedIn()) 356 | wrapper.update() 357 | 358 | history.push('/auth/1') 359 | wrapper.update() 360 | expect(getLocation().pathname).to.equal('/auth/1') 361 | expect(getQueryParams(getLocation())).to.be.empty 362 | 363 | history.push('/auth/2') 364 | wrapper.update() 365 | expect(getLocation().pathname).to.equal('/login') 366 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth/2' }) 367 | }) 368 | 369 | it('can override query param name', () => { 370 | const auth = authWrapper({ 371 | ...defaultConfig, 372 | redirectQueryParamName: 'customRedirect' 373 | }) 374 | const routes = [ 375 | { path: '/login', component: UnprotectedComponent }, 376 | { path: '/auth', component: auth(UnprotectedComponent) } 377 | ] 378 | 379 | const { history, getLocation, wrapper } = setupTest(routes) 380 | 381 | expect(getLocation().pathname).to.equal('/') 382 | expect(getQueryParams(getLocation())).to.be.empty 383 | 384 | history.push('/auth') 385 | wrapper.update() 386 | expect(getLocation().pathname).to.equal('/login') 387 | expect(getQueryParams(getLocation())).to.deep.equal({ customRedirect: '/auth' }) 388 | }) 389 | 390 | it('can pass a selector for redirectPath', () => { 391 | const auth = authWrapper({ 392 | ...defaultConfig, 393 | redirectPath: (state, ownProps) => { 394 | if (!authenticatedSelector(state) && getRouteParams(ownProps).id === '1') { 395 | return '/login/1' 396 | } else { 397 | return '/login/0' 398 | } 399 | } 400 | }) 401 | const routes = [ 402 | { path: '/login/:id', component: UnprotectedComponent }, 403 | { path: '/auth/:id', component: auth(UnprotectedComponent) } 404 | ] 405 | 406 | const { history, getLocation, wrapper } = setupTest(routes) 407 | 408 | expect(getLocation().pathname).to.equal('/') 409 | expect(getQueryParams(getLocation())).to.be.empty 410 | 411 | history.push('/auth/1') 412 | wrapper.update() 413 | expect(getLocation().pathname).to.equal('/login/1') 414 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth/1' }) 415 | 416 | history.push('/auth/2') 417 | wrapper.update() 418 | expect(getLocation().pathname).to.equal('/login/0') 419 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth/2' }) 420 | }) 421 | 422 | it('only redirects once when props change but authentication is constant', () => { 423 | 424 | const redirect = sinon.stub().returns({ type: 'NO_REDIRECT' }) 425 | 426 | const auth = authWrapper({ 427 | ...defaultConfig, 428 | FailureComponent: (props) => , 429 | authenticatedSelector: () => false 430 | }) 431 | 432 | const routes = [ 433 | { path: '/login', component: UnprotectedComponent }, 434 | { path: '/auth', component: auth(UnprotectedComponent) } 435 | ] 436 | 437 | const { history, store, wrapper } = setupTest(routes) 438 | 439 | history.push('/auth') 440 | wrapper.update() 441 | expect(redirect.calledOnce).to.equal(true) 442 | 443 | store.dispatch(userLoggedIn()) 444 | wrapper.update() 445 | expect(redirect.calledOnce).to.equal(true) 446 | }) 447 | 448 | it('redirection preserves query params', () => { 449 | const auth = authWrapper(defaultConfig) 450 | const login = authWrapper({ 451 | ...defaultConfig, 452 | authenticatedSelector: state => !authenticatedSelector(state), 453 | redirectPath: (state, ownProps) => getRedirectQueryParam(ownProps) || '/', 454 | allowRedirectBack: false 455 | }) 456 | const routes = [ 457 | { path: '/login', component: login(UnprotectedComponent) }, 458 | { path: '/protected', component: auth(UnprotectedComponent) } 459 | ] 460 | 461 | const { history, store, getLocation, wrapper } = setupTest(routes) 462 | 463 | expect(getLocation().pathname).to.equal('/') 464 | expect(getQueryParams(getLocation())).to.be.empty 465 | 466 | history.push('/protected?param=foo') 467 | wrapper.update() 468 | expect(getLocation().pathname).to.equal('/login') 469 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/protected?param=foo' }) 470 | 471 | store.dispatch(userLoggedIn()) 472 | wrapper.update() 473 | expect(getLocation().pathname).to.equal('/protected') 474 | expect(getQueryParams(getLocation())).to.deep.equal({ param: 'foo' }) 475 | }) 476 | 477 | it('redirection preserves hash fragment', () => { 478 | const auth = authWrapper(defaultConfig) 479 | const login = authWrapper({ 480 | ...defaultConfig, 481 | authenticatedSelector: state => !authenticatedSelector(state), 482 | redirectPath: (state, ownProps) => getRedirectQueryParam(ownProps) || '/', 483 | allowRedirectBack: false 484 | }) 485 | const routes = [ 486 | { path: '/login', component: login(UnprotectedComponent) }, 487 | { path: '/protected', component: auth(UnprotectedComponent) } 488 | ] 489 | 490 | const { history, store, getLocation, wrapper } = setupTest(routes) 491 | 492 | expect(getLocation().pathname).to.equal('/') 493 | expect(getQueryParams(getLocation())).to.be.empty 494 | 495 | history.push('/protected#foo') 496 | wrapper.update() 497 | expect(getLocation().pathname).to.equal('/login') 498 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/protected#foo' }) 499 | 500 | store.dispatch(userLoggedIn()) 501 | wrapper.update() 502 | expect(getLocation().pathname).to.equal('/protected') 503 | expect(getLocation().hash).to.equal('#foo') 504 | expect(getQueryParams(getLocation())).to.be.empty 505 | }) 506 | 507 | it('Throws invariant when redirectpath is not a function or string', () => { 508 | expect(() => authWrapper({ ...defaultConfig, redirectPath: true })).to.throw(/redirectPath must be either a string or a function/) 509 | expect(() => authWrapper({ ...defaultConfig, redirectPath: 1 })).to.throw(/redirectPath must be either a string or a function/) 510 | expect(() => authWrapper({ ...defaultConfig, redirectPath: [] })).to.throw(/redirectPath must be either a string or a function/) 511 | expect(() => authWrapper({ ...defaultConfig, redirectPath: {} })).to.throw(/redirectPath must be either a string or a function/) 512 | }) 513 | 514 | it('Throws invariant when allowRedirectBack is not a function or boolean', () => { 515 | expect(() => authWrapper({ ...defaultConfig, allowRedirectBack: 'login' })).to.throw(/allowRedirectBack must be either a boolean or a function/) 516 | expect(() => authWrapper({ ...defaultConfig, allowRedirectBack: 1 })).to.throw(/allowRedirectBack must be either a boolean or a function/) 517 | expect(() => authWrapper({ ...defaultConfig, allowRedirectBack: [] })).to.throw(/allowRedirectBack must be either a boolean or a function/) 518 | expect(() => authWrapper({ ...defaultConfig, allowRedirectBack: {} })).to.throw(/allowRedirectBack must be either a boolean or a function/) 519 | }) 520 | 521 | it("Doesn't cause re-render when store changes", () => { 522 | const updatespy = sinon.spy() 523 | 524 | class UpdateComponent extends Component { 525 | componentDidUpdate() { 526 | updatespy() 527 | } 528 | 529 | render() { 530 | return
531 | } 532 | } 533 | 534 | const auth = authWrapper(defaultConfig) 535 | const routes = [ 536 | { path: '/auth', component: auth(UpdateComponent) } 537 | ] 538 | 539 | const { history, store, getLocation, wrapper } = setupTest(routes) 540 | 541 | store.dispatch(userLoggedIn()) 542 | history.push('/auth') 543 | wrapper.update() 544 | 545 | expect(getLocation().pathname).to.equal('/auth') 546 | 547 | expect(updatespy.called).to.be.false 548 | store.dispatch(userLoggedIn()) 549 | wrapper.update() 550 | expect(updatespy.called).to.be.false 551 | }) 552 | }) 553 | } 554 | -------------------------------------------------------------------------------- /test/rrv3-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha, jasmine */ 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import _ from 'lodash' 5 | import createMemoryHistory from 'react-router/lib/createMemoryHistory' 6 | import { routerMiddleware, syncHistoryWithStore, routerActions, routerReducer } from 'react-router-redux' 7 | import { Router, Route } from 'react-router' 8 | import { Provider } from 'react-redux' 9 | import { createStore, applyMiddleware, combineReducers } from 'redux' 10 | import sinon from 'sinon' 11 | import { mount } from 'enzyme' 12 | 13 | import { userLoggedOut, userLoggedIn, userLoggingIn, authenticatedSelector, userReducer, UnprotectedComponent, UnprotectedParentComponent, defaultConfig } from './helpers' 14 | import baseTests from './redirectBase-test' 15 | 16 | import { connectedRouterRedirect, connectedReduxRedirect, createOnEnter } from '../src/history3/redirect' 17 | import locationHelperBuilder from '../src/history3/locationHelper' 18 | 19 | const locationHelper = locationHelperBuilder({}) 20 | 21 | class App extends Component { 22 | static propTypes = { 23 | children: PropTypes.node 24 | }; 25 | 26 | render() { 27 | return ( 28 |
29 | {this.props.children} 30 |
31 | ) 32 | } 33 | } 34 | 35 | const setupReactRouter3Test = (testRoutes) => { 36 | const history = createMemoryHistory() 37 | const rootReducer = combineReducers({ user: userReducer }) 38 | const store = createStore(rootReducer) 39 | const routes = ( 40 | 41 | {testRoutes.map((route, i) => )} 42 | 43 | ) 44 | 45 | const wrapper = mount( 46 | 47 | 48 | {routes} 49 | 50 | 51 | ) 52 | 53 | const getLocation = history.getCurrentLocation 54 | 55 | return { 56 | history, 57 | store, 58 | wrapper, 59 | getLocation 60 | } 61 | } 62 | 63 | const setupReactRouterReduxTest = (testRoutes) => { 64 | const baseHistory = createMemoryHistory() 65 | const middleware = routerMiddleware(baseHistory) 66 | const rootReducer = combineReducers({ user: userReducer, routing: routerReducer }) 67 | const routes = ( 68 | 69 | {testRoutes.map((route, i) => )} 70 | 71 | ) 72 | 73 | const store = createStore( 74 | rootReducer, 75 | applyMiddleware(middleware) 76 | ) 77 | const history = syncHistoryWithStore(baseHistory, store) 78 | 79 | const wrapper = mount( 80 | 81 | 82 | {routes} 83 | 84 | 85 | ) 86 | 87 | const getLocation = history.getCurrentLocation 88 | 89 | return { 90 | history, 91 | store, 92 | wrapper, 93 | getLocation 94 | } 95 | } 96 | 97 | const getRouteParams = (ownProps) => ownProps.routeParams 98 | const getQueryParams = (location) => location.query 99 | 100 | baseTests(setupReactRouter3Test, 'React Router V3', getRouteParams, getQueryParams, 101 | locationHelper.getRedirectQueryParam, connectedRouterRedirect) 102 | 103 | baseTests(setupReactRouterReduxTest, 'React Router V3 with react-router-redux', getRouteParams, getQueryParams, 104 | locationHelper.getRedirectQueryParam, (config) => connectedReduxRedirect({ ...config, redirectAction: routerActions.replace })) 105 | 106 | describe('React Router V3 onEnter', () => { 107 | it('provides an onEnter static function', () => { 108 | let store 109 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) 110 | const authenticatedSelectorSpy = sinon.spy(authenticatedSelector) 111 | const failureRedirectSpy = sinon.spy(() => '/login') 112 | 113 | const onEnter = createOnEnter({ 114 | redirectPath: failureRedirectSpy, 115 | authenticatedSelector: authenticatedSelectorSpy 116 | }) 117 | 118 | const routesOnEnter = [ 119 | { path: 'login', component: UnprotectedComponent }, 120 | { path: 'onEnter', component: UnprotectedComponent, onEnter: connect(onEnter) } 121 | ] 122 | 123 | const { history, store: createdStore, getLocation, wrapper } = setupReactRouter3Test(routesOnEnter) 124 | store = createdStore 125 | 126 | expect(getLocation().pathname).to.equal('/') 127 | expect(getLocation().search).to.equal('') 128 | 129 | // Redirects when not authorized 130 | store.dispatch(userLoggedOut()) 131 | wrapper.update() 132 | // Have to force re-check because wont recheck with store changes 133 | history.push('/') 134 | history.push('/onEnter') 135 | wrapper.update() 136 | expect(authenticatedSelectorSpy.calledOnce).to.be.true 137 | expect(failureRedirectSpy.calledOnce).to.be.true 138 | expect(failureRedirectSpy.firstCall.args[0].user).to.deep.equal(store.getState().user) // cant compare location because its changed 139 | expect(Object.keys(failureRedirectSpy.firstCall.args[1])).to.deep.equal([ 'routes', 'params', 'location' ]) // cant compare location because its changed 140 | expect(getLocation().pathname).to.equal('/login') 141 | expect(getLocation().search).to.equal('?redirect=%2FonEnter') 142 | }) 143 | 144 | it('supports isAuthenticating', () => { 145 | let store 146 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) 147 | const authenticatedSelectorSpy = sinon.spy(authenticatedSelector) 148 | const authenticatingSelectorSpy = sinon.spy(state => state.user.isAuthenticating) 149 | const failureRedirectSpy = sinon.spy(() => '/login') 150 | 151 | const onEnter = createOnEnter({ 152 | redirectPath: failureRedirectSpy, 153 | authenticatedSelector: authenticatedSelectorSpy, 154 | authenticatingSelector: authenticatingSelectorSpy 155 | }) 156 | 157 | const routesOnEnter = [ 158 | { path: 'login', component: UnprotectedComponent }, 159 | { path: 'onEnter', component: UnprotectedComponent, onEnter: connect(onEnter) } 160 | ] 161 | 162 | const { history, store: createdStore, wrapper, getLocation } = setupReactRouter3Test(routesOnEnter) 163 | store = createdStore 164 | 165 | expect(getLocation().pathname).to.equal('/') 166 | expect(getLocation().search).to.equal('') 167 | 168 | // Supports isAuthenticating 169 | store.dispatch(userLoggingIn()) 170 | history.push('/onEnter') 171 | wrapper.update() 172 | const nextState = _.pick(wrapper.find(App).props(), [ 'location', 'params', 'routes' ]) 173 | const storeState = store.getState() 174 | expect(authenticatedSelectorSpy.calledOnce).to.be.true 175 | // Passes store and nextState down to selectors and redirectPath 176 | expect(authenticatedSelectorSpy.calledOnce).to.be.true 177 | expect(authenticatedSelectorSpy.firstCall.args).to.deep.equal([ storeState, nextState ]) 178 | expect(authenticatingSelectorSpy.calledOnce).to.be.true 179 | expect(authenticatingSelectorSpy.firstCall.args).to.deep.equal([ storeState, nextState ]) 180 | expect(getLocation().pathname).to.equal('/onEnter') 181 | 182 | // Redirects when not authorized 183 | store.dispatch(userLoggedOut()) 184 | // Have to force re-check because wont recheck with store changes 185 | history.push('/') 186 | history.push('/onEnter') 187 | wrapper.update() 188 | expect(authenticatingSelectorSpy.calledTwice).to.be.true 189 | expect(authenticatedSelectorSpy.calledTwice).to.be.true 190 | expect(failureRedirectSpy.calledOnce).to.be.true 191 | expect(failureRedirectSpy.firstCall.args[0].user).to.deep.equal(store.getState().user) // cant compare location because its changed 192 | expect(Object.keys(failureRedirectSpy.firstCall.args[1])).to.deep.equal([ 'routes', 'params', 'location' ]) // cant compare location because its changed 193 | expect(getLocation().pathname).to.equal('/login') 194 | expect(getLocation().search).to.equal('?redirect=%2FonEnter') 195 | }) 196 | 197 | it('optionally prevents redirection from a function result', () => { 198 | let store 199 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) 200 | 201 | const onEnter = createOnEnter({ 202 | ...defaultConfig, 203 | authenticatedSelector: () => false, 204 | allowRedirectBack: ({ location }, redirectPath) => location.pathname === '/auth' && redirectPath === '/login' 205 | }) 206 | 207 | const routesOnEnter = [ 208 | { path: '/login', component: UnprotectedComponent }, 209 | { path: '/auth', component: UnprotectedComponent, onEnter: connect(onEnter) }, 210 | { path: '/authNoRedir', component: UnprotectedComponent, onEnter: connect(onEnter) } 211 | ] 212 | 213 | const { history, store: createdStore, getLocation, wrapper } = setupReactRouter3Test(routesOnEnter) 214 | store = createdStore 215 | 216 | store.dispatch(userLoggedIn()) 217 | 218 | history.push('/auth') 219 | wrapper.update() 220 | expect(getLocation().pathname).to.equal('/login') 221 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth' }) 222 | 223 | history.push('/authNoRedir') 224 | wrapper.update() 225 | expect(getLocation().pathname).to.equal('/login') 226 | expect(getQueryParams(getLocation())).to.deep.equal({}) 227 | }) 228 | 229 | it('can pass a selector for redirectPath', () => { 230 | let store 231 | const connect = (fn) => (nextState, replaceState) => fn(store, nextState, replaceState) 232 | 233 | const onEnter = createOnEnter({ 234 | ...defaultConfig, 235 | redirectPath: (state, routerNextState) => { 236 | if (!authenticatedSelector(state) && routerNextState.params.id === '1') { 237 | return '/login/1' 238 | } else { 239 | return '/login/0' 240 | } 241 | } 242 | }) 243 | const routesOnEnter = [ 244 | { path: '/login/:id', component: UnprotectedComponent }, 245 | { path: '/auth/:id', component: UnprotectedComponent, onEnter: connect(onEnter) } 246 | ] 247 | 248 | const { history, store: createdStore, getLocation, wrapper } = setupReactRouter3Test(routesOnEnter) 249 | store = createdStore 250 | 251 | expect(getLocation().pathname).to.equal('/') 252 | expect(getQueryParams(getLocation())).to.be.empty 253 | 254 | history.push('/auth/1') 255 | wrapper.update() 256 | expect(getLocation().pathname).to.equal('/login/1') 257 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth/1' }) 258 | 259 | history.push('/auth/2') 260 | wrapper.update() 261 | expect(getLocation().pathname).to.equal('/login/0') 262 | expect(getQueryParams(getLocation())).to.deep.equal({ redirect: '/auth/2' }) 263 | }) 264 | 265 | it('Throws invariant when redirectpath is not a function or string', () => { 266 | expect(() => createOnEnter({ ...defaultConfig, redirectPath: true })).to.throw(/redirectPath must be either a string or a function/) 267 | expect(() => createOnEnter({ ...defaultConfig, redirectPath: 1 })).to.throw(/redirectPath must be either a string or a function/) 268 | expect(() => createOnEnter({ ...defaultConfig, redirectPath: [] })).to.throw(/redirectPath must be either a string or a function/) 269 | expect(() => createOnEnter({ ...defaultConfig, redirectPath: {} })).to.throw(/redirectPath must be either a string or a function/) 270 | }) 271 | 272 | it('Throws invariant when allowRedirectBack is not a function or boolean', () => { 273 | expect(() => createOnEnter({ ...defaultConfig, allowRedirectBack: 'login' })).to.throw(/allowRedirectBack must be either a boolean or a function/) 274 | expect(() => createOnEnter({ ...defaultConfig, allowRedirectBack: 1 })).to.throw(/allowRedirectBack must be either a boolean or a function/) 275 | expect(() => createOnEnter({ ...defaultConfig, allowRedirectBack: [] })).to.throw(/allowRedirectBack must be either a boolean or a function/) 276 | expect(() => createOnEnter({ ...defaultConfig, allowRedirectBack: {} })).to.throw(/allowRedirectBack must be either a boolean or a function/) 277 | }) 278 | }) 279 | 280 | describe('wrapper React Router V3 Additions', () => { 281 | 282 | it('supports nested routes', () => { 283 | const auth = connectedRouterRedirect(defaultConfig) 284 | 285 | const routes = [ 286 | { path: 'login', component: UnprotectedComponent }, 287 | { path: 'parent', component: auth(UnprotectedParentComponent), childRoutes: [ 288 | { path: 'child', component: auth(UnprotectedComponent) } 289 | ] } 290 | ] 291 | 292 | const { history, store, getLocation, wrapper } = setupReactRouter3Test(routes) 293 | 294 | history.push('/parent/child') 295 | wrapper.update() 296 | expect(getLocation().pathname).to.equal('/login') 297 | expect(getLocation().search).to.equal('?redirect=%2Fparent%2Fchild') 298 | 299 | store.dispatch(userLoggedIn()) 300 | wrapper.update() 301 | 302 | history.push('/parent/child') 303 | wrapper.update() 304 | expect(getLocation().pathname).to.equal('/parent/child') 305 | expect(getLocation().query).to.be.empty 306 | 307 | store.dispatch(userLoggedOut()) 308 | wrapper.update() 309 | expect(getLocation().pathname).to.equal('/login') 310 | expect(getLocation().search).to.equal('?redirect=%2Fparent%2Fchild') 311 | }) 312 | }) 313 | -------------------------------------------------------------------------------- /test/rrv4-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha, jasmine */ 2 | import React from 'react' 3 | import createMemoryHistory from 'history/createMemoryHistory' 4 | import { Router, Route, Switch } from 'react-router' 5 | import { Provider } from 'react-redux' 6 | import { createStore, applyMiddleware, combineReducers } from 'redux' 7 | import { mount } from 'enzyme' 8 | import { parse } from 'query-string' 9 | import { ConnectedRouter, connectRouter, routerMiddleware, replace } from 'connected-react-router' 10 | 11 | import { userReducer } from './helpers' 12 | import baseTests from './redirectBase-test' 13 | 14 | import { connectedRouterRedirect, connectedReduxRedirect } from '../src/history4/redirect' 15 | import locationHelperBuilder from '../src/history4/locationHelper' 16 | 17 | const locationHelper = locationHelperBuilder({}) 18 | 19 | const setupReactRouter4Test = (testRoutes) => { 20 | const history = createMemoryHistory() 21 | const rootReducer = combineReducers({ user: userReducer }) 22 | const store = createStore(rootReducer) 23 | 24 | const wrapper = mount( 25 | 26 | 27 |
28 | 29 | {testRoutes.map((routeConfig, i) => )} 30 | 31 |
32 |
33 |
34 | ) 35 | 36 | const getLocation = () => history.location 37 | 38 | return { 39 | history, 40 | store, 41 | wrapper, 42 | getLocation 43 | } 44 | } 45 | 46 | const setupReactRouterReduxTest = (testRoutes) => { 47 | const history = createMemoryHistory() 48 | const middleware = routerMiddleware(history) 49 | const rootReducer = combineReducers({ user: userReducer, router: connectRouter(history) }) 50 | 51 | const store = createStore( 52 | rootReducer, 53 | applyMiddleware(middleware) 54 | ) 55 | 56 | const wrapper = mount( 57 | 58 | 59 |
60 | 61 | {testRoutes.map((routeConfig, i) => )} 62 | 63 |
64 |
65 |
66 | ) 67 | 68 | const getLocation = () => history.location 69 | 70 | return { 71 | history, 72 | store, 73 | wrapper, 74 | getLocation 75 | } 76 | } 77 | 78 | const getRouteParams = (ownProps) => ownProps.match.params 79 | const getQueryParams = (location) => parse(location.search) 80 | 81 | baseTests(setupReactRouter4Test, 'React Router V4', getRouteParams, getQueryParams, 82 | locationHelper.getRedirectQueryParam, connectedRouterRedirect) 83 | 84 | baseTests(setupReactRouterReduxTest, 'React Router V4 with connected-react-router', getRouteParams, getQueryParams, 85 | locationHelper.getRedirectQueryParam, (config) => connectedReduxRedirect({ ...config, redirectAction: replace })) 86 | --------------------------------------------------------------------------------