├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── actions.js ├── components ├── Login.js ├── Logout.js ├── Navbar.js └── Quotes.js ├── containers └── App.js ├── index.html ├── index.js ├── middleware └── api.js ├── package.json ├── reducers.js ├── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server"] 2 | path = server 3 | url = https://github.com/auth0/nodejs-jwt-authentication-sample -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux JWT Authentication Sample 2 | 3 | This is a sample of how to implement JWT authentication in React and [Redux](https://github.com/rackt/redux) apps. It uses Auth0's [NodeJS JWT Authentication Sample](https://github.com/auth0/nodejs-jwt-authentication-sample) to authenticate users and retrieve quotes from a protected endpoint. 4 | 5 | The sample is well-informed by the official [Redux examples](https://github.com/rackt/redux/tree/master/examples). 6 | 7 | **Check the [auth0-lock](https://github.com/auth0/redux-auth/tree/auth0-lock) branch for the Auth0 specific version** 8 | 9 | ## Installation 10 | 11 | Clone the repo and run the installation commands, each in a new terminal window. 12 | 13 | ```bash 14 | # Get the server submodule 15 | git submodule update --init 16 | 17 | # Install deps in the project root and in the server directory 18 | npm install 19 | cd server && npm install 20 | cd .. 21 | 22 | # Run the server 23 | npm run server 24 | 25 | # New terminal window 26 | npm start 27 | ``` 28 | 29 | The app will be served at `localhost:3000`. 30 | 31 | ## Important Snippets 32 | 33 | Users are authenticated by making a `fetch` request to `localhost:3001/sessions/create`. We have actions setup for this. 34 | 35 | ```js 36 | // actions.js 37 | 38 | // There are three possible states for our login 39 | // process and we need actions for each of them 40 | export const LOGIN_REQUEST = 'LOGIN_REQUEST' 41 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 42 | export const LOGIN_FAILURE = 'LOGIN_FAILURE' 43 | 44 | function requestLogin(creds) { 45 | return { 46 | type: LOGIN_REQUEST, 47 | isFetching: true, 48 | isAuthenticated: false, 49 | creds 50 | } 51 | } 52 | 53 | function receiveLogin(user) { 54 | return { 55 | type: LOGIN_SUCCESS, 56 | isFetching: false, 57 | isAuthenticated: true, 58 | id_token: user.id_token 59 | } 60 | } 61 | 62 | function loginError(message) { 63 | return { 64 | type: LOGIN_FAILURE, 65 | isFetching: false, 66 | isAuthenticated: false, 67 | message 68 | } 69 | } 70 | 71 | // Three possible states for our logout process as well. 72 | // Since we are using JWTs, we just need to remove the token 73 | // from localStorage. These actions are more useful if we 74 | // were calling the API to log the user out 75 | export const LOGOUT_REQUEST = 'LOGOUT_REQUEST' 76 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS' 77 | export const LOGOUT_FAILURE = 'LOGOUT_FAILURE' 78 | 79 | function requestLogout() { 80 | return { 81 | type: LOGOUT_REQUEST, 82 | isFetching: true, 83 | isAuthenticated: true 84 | } 85 | } 86 | 87 | function receiveLogout() { 88 | return { 89 | type: LOGOUT_SUCCESS, 90 | isFetching: false, 91 | isAuthenticated: false 92 | } 93 | } 94 | 95 | // Calls the API to get a token and 96 | // dispatches actions along the way 97 | export function loginUser(creds) { 98 | 99 | let config = { 100 | method: 'POST', 101 | headers: { 'Content-Type':'application/x-www-form-urlencoded' }, 102 | body: `username=${creds.username}&password=${creds.password}` 103 | } 104 | 105 | return dispatch => { 106 | // We dispatch requestLogin to kickoff the call to the API 107 | dispatch(requestLogin(creds)) 108 | return fetch('http://localhost:3001/sessions/create', config) 109 | .then(response => 110 | response.json() 111 | .then(user => ({ user, response })) 112 | ).then(({ user, response }) => { 113 | if (!response.ok) { 114 | // If there was a problem, we want to 115 | // dispatch the error condition 116 | dispatch(loginError(user.message)) 117 | return Promise.reject(user) 118 | } 119 | else { 120 | // If login was successful, set the token in local storage 121 | localStorage.setItem('id_token', user.id_token) 122 | 123 | // Dispatch the success action 124 | dispatch(receiveLogin(user)) 125 | } 126 | }).catch(err => console.log("Error: ", err)) 127 | } 128 | } 129 | 130 | // Logs the user out 131 | export function logoutUser() { 132 | return dispatch => { 133 | dispatch(requestLogout()) 134 | localStorage.removeItem('id_token') 135 | dispatch(receiveLogout()) 136 | } 137 | } 138 | ``` 139 | 140 | We also have actions for retreiving the quotes that uses an API middleware. 141 | 142 | ```js 143 | // middleware/api.js 144 | 145 | const BASE_URL = 'http://localhost:3001/api/' 146 | 147 | function callApi(endpoint, authenticated) { 148 | 149 | let token = localStorage.getItem('id_token') || null 150 | let config = {} 151 | 152 | if(authenticated) { 153 | if(token) { 154 | config = { 155 | headers: { 'Authorization': `Bearer ${token}` } 156 | } 157 | } else { 158 | throw "No token saved!" 159 | } 160 | } 161 | 162 | return fetch(BASE_URL + endpoint, config) 163 | .then(response => 164 | response.text() 165 | .then(text => ({ text, response })) 166 | ).then(({ text, response }) => { 167 | if (!response.ok) { 168 | return Promise.reject(text) 169 | } 170 | 171 | return text 172 | }).catch(err => console.log(err)) 173 | } 174 | 175 | export const CALL_API = Symbol('Call API') 176 | 177 | export default store => next => action => { 178 | 179 | const callAPI = action[CALL_API] 180 | 181 | // So the middleware doesn't get applied to every single action 182 | if (typeof callAPI === 'undefined') { 183 | return next(action) 184 | } 185 | 186 | let { endpoint, types, authenticated } = callAPI 187 | 188 | const [ requestType, successType, errorType ] = types 189 | 190 | // Passing the authenticated boolean back in our data will let us distinguish between normal and secret quotes 191 | return callApi(endpoint, authenticated).then( 192 | response => 193 | next({ 194 | response, 195 | authenticated, 196 | type: successType 197 | }), 198 | error => next({ 199 | error: error.message || 'There was an error.', 200 | type: errorType 201 | }) 202 | ) 203 | } 204 | ``` 205 | 206 | ```js 207 | // actions.js 208 | 209 | // Uses the API middlware to get a quote 210 | export function fetchQuote() { 211 | return { 212 | [CALL_API]: { 213 | endpoint: 'random-quote', 214 | types: [QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE] 215 | } 216 | } 217 | } 218 | 219 | // Same API middlware is used to get a 220 | // secret quote, but we set authenticated 221 | // to true so that the auth header is sent 222 | export function fetchSecretQuote() { 223 | return { 224 | [CALL_API]: { 225 | endpoint: 'protected/random-quote', 226 | authenticated: true, 227 | types: [QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE] 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | The reducers return new objects with the data carried by the actions. 234 | 235 | ```js 236 | // reducers.js 237 | 238 | import { combineReducers } from 'redux' 239 | import { 240 | LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT_SUCCESS, 241 | QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE 242 | } from './actions' 243 | 244 | // The auth reducer. The starting state sets authentication 245 | // based on a token being in local storage. In a real app, 246 | // we would also want a util to check if the token is expired. 247 | function auth(state = { 248 | isFetching: false, 249 | isAuthenticated: localStorage.getItem('id_token') ? true : false 250 | }, action) { 251 | switch (action.type) { 252 | case LOGIN_REQUEST: 253 | return Object.assign({}, state, { 254 | isFetching: true, 255 | isAuthenticated: false, 256 | user: action.creds 257 | }) 258 | case LOGIN_SUCCESS: 259 | return Object.assign({}, state, { 260 | isFetching: false, 261 | isAuthenticated: true, 262 | errorMessage: '' 263 | }) 264 | case LOGIN_FAILURE: 265 | return Object.assign({}, state, { 266 | isFetching: false, 267 | isAuthenticated: false, 268 | errorMessage: action.message 269 | }) 270 | case LOGOUT_SUCCESS: 271 | return Object.assign({}, state, { 272 | isFetching: true, 273 | isAuthenticated: false 274 | }) 275 | default: 276 | return state 277 | } 278 | } 279 | 280 | // The quotes reducer 281 | function quotes(state = { 282 | isFetching: false, 283 | quote: '', 284 | authenticated: false 285 | }, action) { 286 | switch (action.type) { 287 | case QUOTE_REQUEST: 288 | return Object.assign({}, state, { 289 | isFetching: true 290 | }) 291 | case QUOTE_SUCCESS: 292 | return Object.assign({}, state, { 293 | isFetching: false, 294 | quote: action.response, 295 | authenticated: action.authenticated || false 296 | }) 297 | case QUOTE_FAILURE: 298 | return Object.assign({}, state, { 299 | isFetching: false 300 | }) 301 | default: 302 | return state 303 | } 304 | } 305 | 306 | // We combine the reducers here so that they 307 | // can be left split apart above 308 | const quotesApp = combineReducers({ 309 | auth, 310 | quotes 311 | }) 312 | 313 | export default quotesApp 314 | ``` 315 | ## What is Auth0? 316 | 317 | Auth0 helps you to: 318 | 319 | * Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. 320 | * Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. 321 | * Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. 322 | * Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. 323 | * Analytics of how, when and where users are logging in. 324 | * Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). 325 | 326 | ## Create a Free Auth0 Account 327 | 328 | 1. Go to [Auth0](https://auth0.com) and click Sign Up. 329 | 2. Use Google, GitHub or Microsoft Account to login. 330 | 331 | ## Issue Reporting 332 | 333 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 334 | 335 | ## Author 336 | 337 | [Auth0](https://auth0.com) 338 | 339 | ## License 340 | 341 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 342 | -------------------------------------------------------------------------------- /actions.js: -------------------------------------------------------------------------------- 1 | // The middleware to call the API for quotes 2 | import { CALL_API } from './middleware/api' 3 | 4 | // There are three possible states for our login 5 | // process and we need actions for each of them 6 | export const LOGIN_REQUEST = 'LOGIN_REQUEST' 7 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS' 8 | export const LOGIN_FAILURE = 'LOGIN_FAILURE' 9 | 10 | function requestLogin(creds) { 11 | return { 12 | type: LOGIN_REQUEST, 13 | isFetching: true, 14 | isAuthenticated: false, 15 | creds 16 | } 17 | } 18 | 19 | function receiveLogin(user) { 20 | return { 21 | type: LOGIN_SUCCESS, 22 | isFetching: false, 23 | isAuthenticated: true, 24 | id_token: user.id_token 25 | } 26 | } 27 | 28 | function loginError(message) { 29 | return { 30 | type: LOGIN_FAILURE, 31 | isFetching: false, 32 | isAuthenticated: false, 33 | message 34 | } 35 | } 36 | 37 | // Three possible states for our logout process as well. 38 | // Since we are using JWTs, we just need to remove the token 39 | // from localStorage. These actions are more useful if we 40 | // were calling the API to log the user out 41 | export const LOGOUT_REQUEST = 'LOGOUT_REQUEST' 42 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS' 43 | export const LOGOUT_FAILURE = 'LOGOUT_FAILURE' 44 | 45 | function requestLogout() { 46 | return { 47 | type: LOGOUT_REQUEST, 48 | isFetching: true, 49 | isAuthenticated: true 50 | } 51 | } 52 | 53 | function receiveLogout() { 54 | return { 55 | type: LOGOUT_SUCCESS, 56 | isFetching: false, 57 | isAuthenticated: false 58 | } 59 | } 60 | 61 | // Calls the API to get a token and 62 | // dispatches actions along the way 63 | export function loginUser(creds) { 64 | 65 | let config = { 66 | method: 'POST', 67 | headers: { 'Content-Type':'application/x-www-form-urlencoded' }, 68 | body: `username=${creds.username}&password=${creds.password}` 69 | } 70 | 71 | return dispatch => { 72 | // We dispatch requestLogin to kickoff the call to the API 73 | dispatch(requestLogin(creds)) 74 | return fetch('http://localhost:3001/sessions/create', config) 75 | .then(response => 76 | response.json() 77 | .then(user => ({ user, response })) 78 | ).then(({ user, response }) => { 79 | if (!response.ok) { 80 | // If there was a problem, we want to 81 | // dispatch the error condition 82 | dispatch(loginError(user.message)) 83 | return Promise.reject(user) 84 | } 85 | else { 86 | // If login was successful, set the token in local storage 87 | localStorage.setItem('id_token', user.id_token) 88 | 89 | // Dispatch the success action 90 | dispatch(receiveLogin(user)) 91 | } 92 | }).catch(err => console.log("Error: ", err)) 93 | } 94 | } 95 | 96 | // Logs the user out 97 | export function logoutUser() { 98 | return dispatch => { 99 | dispatch(requestLogout()) 100 | localStorage.removeItem('id_token') 101 | dispatch(receiveLogout()) 102 | } 103 | } 104 | 105 | export const QUOTE_REQUEST = 'QUOTE_REQUEST' 106 | export const QUOTE_SUCCESS = 'QUOTE_SUCCESS' 107 | export const QUOTE_FAILURE = 'QUOTE_FAILURE' 108 | 109 | // Uses the API middlware to get a quote 110 | export function fetchQuote() { 111 | return { 112 | [CALL_API]: { 113 | endpoint: 'random-quote', 114 | types: [QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE] 115 | } 116 | } 117 | } 118 | 119 | // Same API middlware is used to get a 120 | // secret quote, but we set authenticated 121 | // to true so that the auth header is sent 122 | export function fetchSecretQuote() { 123 | return { 124 | [CALL_API]: { 125 | endpoint: 'protected/random-quote', 126 | authenticated: true, 127 | types: [QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE] 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Login extends Component { 4 | 5 | render() { 6 | const { errorMessage } = this.props 7 | 8 | return ( 9 |
10 | 11 | 12 | 15 | 16 | {errorMessage && 17 |

{errorMessage}

18 | } 19 |
20 | ) 21 | } 22 | 23 | handleClick(event) { 24 | const username = this.refs.username 25 | const password = this.refs.password 26 | const creds = { username: username.value.trim(), password: password.value.trim() } 27 | this.props.onLoginClick(creds) 28 | } 29 | } 30 | 31 | Login.propTypes = { 32 | onLoginClick: PropTypes.func.isRequired, 33 | errorMessage: PropTypes.string 34 | } -------------------------------------------------------------------------------- /components/Logout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Logout extends Component { 4 | 5 | render() { 6 | const { onLogoutClick } = this.props 7 | 8 | return ( 9 | 12 | ) 13 | } 14 | 15 | } 16 | 17 | Logout.propTypes = { 18 | onLogoutClick: PropTypes.func.isRequired 19 | } -------------------------------------------------------------------------------- /components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Login from './Login' 3 | import Logout from './Logout' 4 | import { loginUser, logoutUser } from '../actions' 5 | 6 | export default class Navbar extends Component { 7 | 8 | render() { 9 | const { dispatch, isAuthenticated, errorMessage } = this.props 10 | 11 | return ( 12 | 31 | ) 32 | } 33 | 34 | } 35 | 36 | Navbar.propTypes = { 37 | dispatch: PropTypes.func.isRequired, 38 | isAuthenticated: PropTypes.bool.isRequired, 39 | errorMessage: PropTypes.string 40 | } -------------------------------------------------------------------------------- /components/Quotes.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | export default class Quotes extends Component { 4 | 5 | render() { 6 | const { onQuoteClick, onSecretQuoteClick, isAuthenticated, quote, isSecretQuote } = this.props 7 | 8 | return ( 9 |
10 |
11 | 14 |
15 | 16 | { isAuthenticated && 17 |
18 | 21 |
22 | } 23 | 24 |
25 | { quote && !isSecretQuote && 26 |
27 |
{quote}
28 |
29 | } 30 | 31 | { quote && isAuthenticated && isSecretQuote && 32 |
33 | Secret Quote 34 |
35 |
36 | {quote} 37 |
38 |
39 | } 40 |
41 |
42 | ) 43 | } 44 | } 45 | 46 | Quotes.propTypes = { 47 | onQuoteClick: PropTypes.func.isRequired, 48 | onSecretQuoteClick: PropTypes.func.isRequired, 49 | isAuthenticated: PropTypes.bool.isRequired, 50 | quote: PropTypes.string, 51 | isSecretQuote: PropTypes.bool.isRequired 52 | } -------------------------------------------------------------------------------- /containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { loginUser, fetchQuote, fetchSecretQuote } from '../actions' 4 | import Login from '../components/Login' 5 | import Navbar from '../components/Navbar' 6 | import Quotes from '../components/Quotes' 7 | 8 | class App extends Component { 9 | 10 | render() { 11 | const { dispatch, quote, isAuthenticated, errorMessage, isSecretQuote } = this.props 12 | return ( 13 |
14 | 19 |
20 | dispatch(fetchQuote())} 22 | onSecretQuoteClick={() => dispatch(fetchSecretQuote())} 23 | isAuthenticated={isAuthenticated} 24 | quote={quote} 25 | isSecretQuote={isSecretQuote} 26 | /> 27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | App.propTypes = { 34 | dispatch: PropTypes.func.isRequired, 35 | quote: PropTypes.string, 36 | isAuthenticated: PropTypes.bool.isRequired, 37 | errorMessage: PropTypes.string, 38 | isSecretQuote: PropTypes.bool.isRequired 39 | } 40 | 41 | function mapStateToProps(state) { 42 | 43 | const { quotes, auth } = state 44 | const { quote, authenticated } = quotes 45 | const { isAuthenticated, errorMessage } = auth 46 | 47 | return { 48 | quote, 49 | isSecretQuote: authenticated, 50 | isAuthenticated, 51 | errorMessage 52 | } 53 | } 54 | 55 | export default connect(mapStateToProps)(App) 56 | 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux Auth 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import App from './containers/App' 6 | import quotesApp from './reducers' 7 | import thunkMiddleware from 'redux-thunk' 8 | import api from './middleware/api' 9 | 10 | let createStoreWithMiddleware = applyMiddleware(thunkMiddleware, api)(createStore) 11 | 12 | let store = createStoreWithMiddleware(quotesApp) 13 | 14 | let rootElement = document.getElementById('root') 15 | 16 | render( 17 | 18 | 19 | , 20 | rootElement 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /middleware/api.js: -------------------------------------------------------------------------------- 1 | const BASE_URL = 'http://localhost:3001/api/' 2 | 3 | function callApi(endpoint, authenticated) { 4 | 5 | let token = localStorage.getItem('id_token') || null 6 | let config = {} 7 | 8 | if(authenticated) { 9 | if(token) { 10 | config = { 11 | headers: { 'Authorization': `Bearer ${token}` } 12 | } 13 | } else { 14 | throw "No token saved!" 15 | } 16 | } 17 | 18 | return fetch(BASE_URL + endpoint, config) 19 | .then(response => 20 | response.text() 21 | .then(text => ({ text, response })) 22 | ).then(({ text, response }) => { 23 | if (!response.ok) { 24 | return Promise.reject(text) 25 | } 26 | 27 | return text 28 | }).catch(err => console.log(err)) 29 | } 30 | 31 | export const CALL_API = Symbol('Call API') 32 | 33 | export default store => next => action => { 34 | 35 | const callAPI = action[CALL_API] 36 | 37 | // So the middleware doesn't get applied to every single action 38 | if (typeof callAPI === 'undefined') { 39 | return next(action) 40 | } 41 | 42 | let { endpoint, types, authenticated } = callAPI 43 | 44 | const [ requestType, successType, errorType ] = types 45 | 46 | // Passing the authenticated boolean back in our data will let us distinguish between normal and secret quotes 47 | return callApi(endpoint, authenticated).then( 48 | response => 49 | next({ 50 | response, 51 | authenticated, 52 | type: successType 53 | }), 54 | error => next({ 55 | error: error.message || 'There was an error.', 56 | type: errorType 57 | }) 58 | ) 59 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-auth", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "scripts": { 6 | "server": "node server/server.js", 7 | "start": "node server.js" 8 | }, 9 | "author": "Auth0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "react": "^0.14.3", 13 | "react-dom": "^0.14.3", 14 | "react-redux": "^4.0.4", 15 | "redux": "^3.0.5", 16 | "redux-thunk": "^0.1.0" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^5.6.18", 20 | "babel-loader": "^5.1.4", 21 | "babel-plugin-react-transform": "^1.1.0", 22 | "express": "^4.13.3", 23 | "webpack": "^1.9.11", 24 | "webpack-dev-middleware": "^1.2.0", 25 | "webpack-hot-middleware": "^2.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { 3 | LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT_SUCCESS, 4 | QUOTE_REQUEST, QUOTE_SUCCESS, QUOTE_FAILURE 5 | } from './actions' 6 | 7 | // The auth reducer. The starting state sets authentication 8 | // based on a token being in local storage. In a real app, 9 | // we would also want a util to check if the token is expired. 10 | function auth(state = { 11 | isFetching: false, 12 | isAuthenticated: localStorage.getItem('id_token') ? true : false 13 | }, action) { 14 | switch (action.type) { 15 | case LOGIN_REQUEST: 16 | return Object.assign({}, state, { 17 | isFetching: true, 18 | isAuthenticated: false, 19 | user: action.creds 20 | }) 21 | case LOGIN_SUCCESS: 22 | return Object.assign({}, state, { 23 | isFetching: false, 24 | isAuthenticated: true, 25 | errorMessage: '' 26 | }) 27 | case LOGIN_FAILURE: 28 | return Object.assign({}, state, { 29 | isFetching: false, 30 | isAuthenticated: false, 31 | errorMessage: action.message 32 | }) 33 | case LOGOUT_SUCCESS: 34 | return Object.assign({}, state, { 35 | isFetching: true, 36 | isAuthenticated: false 37 | }) 38 | default: 39 | return state 40 | } 41 | } 42 | 43 | // The quotes reducer 44 | function quotes(state = { 45 | isFetching: false, 46 | quote: '', 47 | authenticated: false 48 | }, action) { 49 | switch (action.type) { 50 | case QUOTE_REQUEST: 51 | return Object.assign({}, state, { 52 | isFetching: true 53 | }) 54 | case QUOTE_SUCCESS: 55 | return Object.assign({}, state, { 56 | isFetching: false, 57 | quote: action.response, 58 | authenticated: action.authenticated || false 59 | }) 60 | case QUOTE_FAILURE: 61 | return Object.assign({}, state, { 62 | isFetching: false 63 | }) 64 | default: 65 | return state 66 | } 67 | } 68 | 69 | // We combine the reducers here so that they 70 | // can be left split apart above 71 | const quotesApp = combineReducers({ 72 | auth, 73 | quotes 74 | }) 75 | 76 | export default quotesApp -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin(), 18 | new webpack.NoErrorsPlugin() 19 | ], 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | }, { 27 | test: /\.css?$/, 28 | loaders: [ 'style', 'raw' ], 29 | include: __dirname 30 | }] 31 | } 32 | } --------------------------------------------------------------------------------