├── .babelrc ├── .eslintrc.yaml ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── README.md ├── eslint ├── eslint-defaults.yaml ├── eslint-es2015.yaml ├── eslint-jsx.yaml └── eslint-node-commonjs.yaml ├── examples ├── .eslintrc.yaml ├── bocoup │ ├── app.jsx │ ├── auth.js │ └── index.html ├── index.html ├── server.js └── webpack.config.babel.js ├── package.json ├── src ├── .eslintrc-test.yaml ├── .eslintrc.yaml ├── auth.js ├── auth.test.js ├── bind-methods.js ├── bind-methods.test.js └── index.js └── tools ├── .eslintrc.yaml ├── gruntfile.js └── test-globals.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-runtime"], 3 | "presets": ["es2015"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "./eslint/eslint-defaults.yaml" 4 | - "./eslint/eslint-node-commonjs.yaml" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | sudo: false 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-register'); 4 | 5 | module.exports = function(grunt) { 6 | module.exports.grunt = grunt; 7 | require('./tools/gruntfile'); 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-router-oauth 2 | 3 | > Basic oauth support for react-router 4 | 5 | [![Build Status](https://travis-ci.org/bocoup/react-router-oauth.svg?branch=master)](https://travis-ci.org/bocoup/react-router-oauth) 6 | [![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) 7 | 8 | _Work in progress!_ 9 | 10 | ## Examples 11 | 12 | 1. Clone this repo 13 | 1. Install project dependencies with `npm install` 14 | 1. Start the development web server with `npm start` 15 | 1. Browse to 16 | -------------------------------------------------------------------------------- /eslint/eslint-defaults.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | 4 | # Possible Errors 5 | 6 | comma-dangle: 7 | - 2 8 | - always-multiline 9 | no-cond-assign: 10 | - 2 11 | - except-parens 12 | no-console: 0 13 | no-constant-condition: 1 14 | no-control-regex: 2 15 | no-debugger: 1 16 | no-dupe-args: 2 17 | no-dupe-keys: 2 18 | no-duplicate-case: 2 19 | no-empty-character-class: 2 20 | no-empty: 1 21 | no-ex-assign: 2 22 | no-extra-boolean-cast: 2 23 | no-extra-parens: 24 | - 2 25 | - functions 26 | no-extra-semi: 2 27 | no-func-assign: 2 28 | no-inner-declarations: 2 29 | no-invalid-regexp: 2 30 | no-irregular-whitespace: 2 31 | no-negated-in-lhs: 2 32 | no-obj-calls: 2 33 | no-regex-spaces: 2 34 | no-sparse-arrays: 2 35 | no-unreachable: 1 36 | use-isnan: 2 37 | valid-jsdoc: 38 | - 2 39 | - prefer: 40 | return: returns 41 | valid-typeof: 2 42 | no-unexpected-multiline: 2 43 | 44 | # Best Practices 45 | 46 | accessor-pairs: 2 47 | block-scoped-var: 2 48 | complexity: 0 49 | consistent-return: 2 50 | curly: 51 | - 2 52 | - all 53 | default-case: 2 54 | dot-notation: 2 55 | dot-location: 56 | - 2 57 | - property 58 | eqeqeq: 59 | - 2 60 | - "allow-null" 61 | guard-for-in: 0 62 | no-alert: 2 63 | no-caller: 2 64 | no-div-regex: 0 65 | no-else-return: 2 66 | no-empty-label: 2 67 | no-eq-null: 0 68 | no-eval: 2 69 | no-extend-native: 0 70 | no-extra-bind: 2 71 | no-fallthrough: 2 72 | no-floating-decimal: 2 73 | no-implicit-coercion: 2 74 | no-implied-eval: 2 75 | no-invalid-this: 0 76 | no-iterator: 2 77 | no-labels: 0 78 | no-lone-blocks: 2 79 | no-loop-func: 2 80 | no-multi-spaces: 2 81 | no-multi-str: 2 82 | no-native-reassign: 2 83 | no-new-func: 0 84 | no-new-wrappers: 2 85 | no-new: 2 86 | no-octal-escape: 2 87 | no-octal: 2 88 | no-param-reassign: 0 89 | no-process-env: 0 90 | no-proto: 2 91 | no-redeclare: 92 | - 2 93 | - builtinGlobals: false 94 | no-return-assign: 2 95 | no-script-url: 2 96 | no-self-compare: 2 97 | no-sequences: 2 98 | no-throw-literal: 2 99 | no-unused-expressions: 2 100 | no-useless-call: 2 101 | no-void: 2 102 | no-warning-comments: 1 103 | no-with: 2 104 | radix: 2 105 | vars-on-top: 0 106 | wrap-iife: 107 | - 2 108 | - inside 109 | yoda: 110 | - 2 111 | - never 112 | 113 | # Strict Mode 114 | 115 | strict: 116 | - 2 117 | - function 118 | 119 | # Variables 120 | 121 | init-declarations: 0 122 | no-catch-shadow: 0 123 | no-delete-var: 2 124 | no-label-var: 2 125 | no-shadow-restricted-names: 2 126 | no-shadow: 2 127 | no-undef-init: 2 128 | no-undef: 2 129 | no-undefined: 2 130 | no-unused-vars: 131 | - 2 132 | - vars: all 133 | args: none 134 | no-use-before-define: 2 135 | 136 | # Stylistic Issues 137 | 138 | array-bracket-spacing: 139 | - 2 140 | - never 141 | brace-style: 142 | - 2 143 | - stroustrup 144 | - allowSingleLine: true 145 | camelcase: 146 | - 2 147 | - properties: never 148 | comma-spacing: 149 | - 2 150 | - before: false 151 | after: true 152 | comma-style: 153 | - 2 154 | - last 155 | computed-property-spacing: 156 | - 2 157 | - never 158 | consistent-this: 159 | - 2 160 | - that 161 | eol-last: 0 162 | func-names: 0 163 | func-style: 0 164 | id-length: 0 165 | id-match: 0 166 | indent: 167 | - 2 168 | - 2 169 | - SwitchCase: 1 170 | VariableDeclarator: 2 171 | key-spacing: 172 | - 2 173 | - beforeColon: false 174 | afterColon: true 175 | lines-around-comment: 176 | - 2 177 | - beforeBlockComment: true 178 | linebreak-style: 179 | - 2 180 | - unix 181 | max-len: 182 | - 1 183 | - 120 184 | - 4 185 | max-nested-callbacks: 0 186 | new-cap: 187 | - 2 188 | - newIsCap: true 189 | capIsNew: true 190 | new-parens: 0 191 | newline-after-var: 0 192 | no-array-constructor: 2 193 | no-continue: 0 194 | no-inline-comments: 0 195 | no-lonely-if: 2 196 | no-mixed-spaces-and-tabs: 2 197 | no-multiple-empty-lines: 198 | - 2 199 | - max: 2 200 | no-nested-ternary: 0 201 | no-new-object: 2 202 | no-spaced-func: 2 203 | no-ternary: 0 204 | no-trailing-spaces: 2 205 | no-underscore-dangle: 0 206 | no-unneeded-ternary: 2 207 | object-curly-spacing: 208 | - 2 209 | - never 210 | one-var: 211 | - 2 212 | - uninitialized: always 213 | initialized: never 214 | operator-assignment: 215 | - 2 216 | - always 217 | operator-linebreak: 218 | - 2 219 | - after 220 | padded-blocks: 0 221 | quote-props: 222 | - 2 223 | - as-needed 224 | - {keywords: false} 225 | quotes: 226 | - 2 227 | - single 228 | - avoid-escape 229 | semi-spacing: 230 | - 2 231 | - before: false 232 | after: true 233 | semi: 234 | - 2 235 | - always 236 | sort-vars: 0 237 | space-after-keywords: 238 | - 2 239 | - always 240 | space-before-blocks: 241 | - 2 242 | - always 243 | space-before-function-paren: 244 | - 2 245 | - never 246 | space-in-parens: 247 | - 2 248 | - never 249 | space-infix-ops: 250 | - 2 251 | - int32Hint: false 252 | space-return-throw-case: 2 253 | space-unary-ops: 254 | - 2 255 | - words: true 256 | nonwords: false 257 | spaced-comment: 258 | - 2 259 | - always 260 | wrap-regex: 0 261 | -------------------------------------------------------------------------------- /eslint/eslint-es2015.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ecmaFeatures: 3 | modules: true 4 | 5 | env: 6 | es6: true 7 | 8 | rules: 9 | # General but applicable here 10 | 11 | no-inner-declarations: 0 12 | no-iterator: 0 13 | 14 | # ECMAScript 6 15 | 16 | arrow-parens: 17 | - 2 18 | - as-needed 19 | arrow-spacing: 20 | - 2 21 | - before: true 22 | after: true 23 | constructor-super: 2 24 | generator-star-spacing: 25 | - 2 26 | - before 27 | no-class-assign: 2 28 | no-const-assign: 2 29 | no-this-before-super: 2 30 | no-var: 2 31 | object-shorthand: 32 | - 2 33 | - always 34 | prefer-const: 2 35 | prefer-spread: 2 36 | prefer-reflect: 0 37 | require-yield: 2 38 | -------------------------------------------------------------------------------- /eslint/eslint-jsx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | plugins: 3 | - react 4 | 5 | ecmaFeatures: 6 | jsx: true 7 | 8 | rules: 9 | react/display-name: 0 10 | react/jsx-boolean-value: 11 | - 2 12 | - always 13 | react/jsx-closing-bracket-location: 0 14 | react/jsx-curly-spacing: 15 | - 2 16 | - never 17 | react/jsx-indent-props: 18 | - 2 19 | - 2 20 | react/jsx-max-props-per-line: 21 | - 1 22 | - 23 | maximum: 3 24 | react/jsx-no-duplicate-props: 25 | - 2 26 | - ignoreCase: false 27 | react/jsx-no-literals: 0 28 | react/jsx-no-undef: 2 29 | jsx-quotes: 30 | - 2 31 | - prefer-double 32 | react/jsx-sort-prop-types: 0 33 | react/jsx-sort-props: 0 34 | react/jsx-uses-react: 2 35 | react/jsx-uses-vars: 2 36 | react/no-danger: 2 37 | react/no-did-mount-set-state: 38 | - 2 39 | - allow-in-func 40 | react/no-did-update-set-state: 2 41 | react/no-multi-comp: 0 42 | react/no-set-state: 0 43 | react/no-unknown-property: 2 44 | react/prop-types: 1 45 | react/react-in-jsx-scope: 2 46 | react/require-extension: 47 | - 2 48 | - extensions: 49 | - ".js" 50 | - ".jsx" 51 | react/self-closing-comp: 2 52 | react/sort-comp: 2 53 | react/wrap-multilines: 2 54 | -------------------------------------------------------------------------------- /eslint/eslint-node-commonjs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | node: true 4 | 5 | ecmaFeatures: 6 | blockBindings: true 7 | 8 | rules: 9 | 10 | # General but applicable here 11 | 12 | strict: 13 | - 2 14 | - global 15 | 16 | # Node.js and CommonJS 17 | 18 | callback-return: 19 | - 2 20 | - [ callback, cb, done, next ] 21 | handle-callback-err: 22 | - 2 23 | - "^err(?:or)?$" 24 | no-mixed-requires: 0 25 | no-new-require: 2 26 | no-path-concat: 2 27 | no-process-exit: 2 28 | no-restricted-modules: 0 29 | no-sync: 0 30 | -------------------------------------------------------------------------------- /examples/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | browser: true 4 | extends: 5 | - "../eslint/eslint-defaults.yaml" 6 | - "../eslint/eslint-es2015.yaml" 7 | - "../eslint/eslint-jsx.yaml" 8 | -------------------------------------------------------------------------------- /examples/bocoup/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {render} from 'react-dom'; 3 | import {Router, Route, IndexRoute, Link, browserHistory} from 'react-router'; 4 | 5 | import auth from './auth'; 6 | 7 | const Login = ({location: {state}}) => { 8 | const redirectTo = state && state.nextPathname; 9 | return ( 10 |
11 |

You must log in to use this app!

12 | Log In 13 |
14 | ); 15 | }; 16 | 17 | const App = ({children}) => ( 18 |
19 |

Bocoup API Auth Example

20 | 21 |

Content

22 | {children} 23 |
24 | ); 25 | 26 | const AuthStatus = () => ( 27 | 28 | You are currently logged in. Log out 29 | 30 | ); 31 | 32 | const Index = () => ( 33 |
34 |

This is the index page.

35 |

You should be able to log out from this page, and then log back in.

36 |

Visit the subpage to test deep-link 37 | redirection and handling API errors.

38 |
39 | ); 40 | 41 | function simulateApiError(event) { 42 | event.preventDefault(); 43 | // This would normally be done in the error callback of an API Ajax request: 44 | auth.logout({redirectBack: true}); 45 | // const {token} = auth.getCredentials(); 46 | // request 47 | // .get('/some-request') 48 | // .set('Authorization', `Bearer ${token}`) 49 | // .then(({body}) => { 50 | // console.log(body); 51 | // }, err => { 52 | // if (err.status === 401) { 53 | // auth.logout({redirectBack: true}); 54 | // } 55 | // throw err; 56 | // }); 57 | } 58 | 59 | const SubPage = () => ( 60 |
61 |

This is the subpage.

62 |

Deep-link Redirection

63 |
    64 |
  1. Note the URL to this page, and copy it to the clipboard.
  2. 65 |
  3. Click the "Log out" link.
  4. 66 |
  5. Paste the URL you copied into the address bar. You should be redirected to the login page.
  6. 67 |
  7. Click the "Log in" link.
  8. 68 |
  9. You should be redirected to GitHub*, then back to the original page, with the query string intact.
  10. 69 |
70 |

Handling API 401 Errors

71 |
    72 |
  1. Note the URL to this page.
  2. 73 |
  3. Click Simulate an API error.
  4. 74 |
  5. You should be redirected to the login page.
  6. 75 |
  7. Click the "Log in" link.
  8. 76 |
  9. You should be redirected to GitHub*, then back to the original page, with the query string intact.
  10. 77 |
78 |

* Unless the Bocoup API remembers you from a previous login.

79 |

Return to the index.

80 |
81 | ); 82 | 83 | const routes = ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | 93 | render(routes, document.getElementById('root')); 94 | -------------------------------------------------------------------------------- /examples/bocoup/auth.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string'; 2 | import {browserHistory} from 'react-router'; 3 | import Auth from 'react-router-oauth'; 4 | 5 | // A few app defaults. 6 | const loginRoute = '/login'; 7 | const loggedInRoute = '/'; 8 | 9 | // Not actually used by the auth system, but it's related. 10 | function authUrl(redirectTo) { 11 | const API_AUTH_URL = 'https://api.bocoup.com/v3/auth/authenticate'; 12 | const provider = 'github'; 13 | const referer = this.getBaseUrl(redirectTo); 14 | const search = queryString.stringify({provider, referer}); 15 | return `${API_AUTH_URL}?${search}`; 16 | } 17 | 18 | // If auth is required for the current route, but the user is not already 19 | // authed, this function will be called. If it returns an object, that object 20 | // will be used to log the user in. 21 | function parseCredentials() { 22 | // Get auth params from the query string. 23 | const params = queryString.parse(location.search); 24 | const {access_token: token, id} = params; 25 | if (token && id) { 26 | // Remove related params from the query object. 27 | delete params.access_token; 28 | delete params.id; 29 | // Replace the current page with the same page, minus the auth query params. 30 | let search = queryString.stringify(params); 31 | if (search) { search = `?${search}`; } 32 | history.replaceState('', {}, `${location.pathname}${search}`); 33 | // The returned object will be used to login. 34 | return {token, id}; 35 | } 36 | } 37 | 38 | export default new Auth({ 39 | browserHistory, 40 | loginRoute, 41 | loggedInRoute, 42 | authUrl, 43 | parseCredentials, 44 | }); 45 | -------------------------------------------------------------------------------- /examples/bocoup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bocoup API Auth Example 5 | 6 | 7 | 8 | « Return to Examples Index 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 |

Examples

8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | // Based on the examples in https://github.com/rackt/react-router 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import express from 'express'; 6 | import rewrite from 'express-urlrewrite'; 7 | import webpack from 'webpack'; 8 | import webpackDevMiddleware from 'webpack-dev-middleware'; 9 | import webpackConfig from './webpack.config.babel'; 10 | 11 | const app = express(); 12 | const compiler = webpack(webpackConfig); 13 | 14 | app.use(webpackDevMiddleware(compiler, { 15 | publicPath: '/__build__/', 16 | stats: { 17 | colors: true, 18 | }, 19 | })); 20 | 21 | fs.readdirSync(__dirname).forEach(file => { 22 | const stat = fs.statSync(path.join(__dirname, file)); 23 | if (stat.isDirectory()) { 24 | app.use(rewrite(`/${file}/*`, `/${file}/index.html`)); 25 | } 26 | }); 27 | 28 | app.use(express.static(__dirname)); 29 | 30 | app.listen(8080, function() { 31 | console.log('Server listening on http://localhost:8080, Ctrl+C to stop.'); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | // Based on the examples in https://github.com/rackt/react-router 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import webpack from 'webpack'; 6 | 7 | const getPath = (...args) => path.join(__dirname, ...args); 8 | 9 | export default { 10 | devtool: 'cheap-module-eval-source-map', 11 | 12 | resolve: { 13 | extensions: ['', '.js', '.jsx'], 14 | alias: { 15 | 'react-router-oauth': getPath('../build'), 16 | }, 17 | }, 18 | 19 | entry: fs.readdirSync(getPath()).reduce((entries, dir) => { 20 | const stat = fs.statSync(getPath(dir)); 21 | if (stat.isDirectory()) { 22 | entries[dir] = getPath(dir, 'app'); 23 | } 24 | return entries; 25 | }, {}), 26 | 27 | output: { 28 | path: getPath('__build__'), 29 | filename: '[name].js', 30 | chunkFilename: '[id].chunk.js', 31 | publicPath: '/__build__/', 32 | }, 33 | 34 | module: { 35 | preLoaders: [ 36 | { 37 | test: /\.jsx?$/, 38 | loader: 'eslint-loader', 39 | include: getPath(), 40 | }, 41 | ], 42 | loaders: [ 43 | { 44 | test: /\.jsx?$/, 45 | loader: 'babel', 46 | include: getPath(), 47 | query: { 48 | presets: ['react', 'es2015'], 49 | }, 50 | }, 51 | ], 52 | }, 53 | 54 | plugins: [ 55 | new webpack.optimize.CommonsChunkPlugin('shared.js'), 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-oauth", 3 | "description": "Basic oauth support for react-router", 4 | "version": "0.2.0", 5 | "dependencies": { 6 | "babel-plugin-transform-runtime": "^6.4.0", 7 | "babel-preset-es2015": "^6.3.13", 8 | "babel-runtime": "^6.5.0" 9 | }, 10 | "main": "build/index", 11 | "files": [ 12 | "build" 13 | ], 14 | "scripts": { 15 | "prepublish": "grunt test build", 16 | "build": "grunt build", 17 | "start": "babel-node examples/server", 18 | "test": "grunt test" 19 | }, 20 | "devDependencies": { 21 | "babel": "^6.5.1", 22 | "babel-cli": "^6.5.1", 23 | "babel-core": "^6.5.1", 24 | "babel-loader": "^6.2.2", 25 | "babel-preset-es2015": "^6.5.0", 26 | "babel-preset-react": "^6.5.0", 27 | "babel-register": "^6.4.3", 28 | "chai": "^3.4.1", 29 | "eslint-loader": "^1.2.1", 30 | "eslint-plugin-react": "^3.16.1", 31 | "express": "^4.13.4", 32 | "express-urlrewrite": "^1.2.0", 33 | "grunt": "^0.4.5", 34 | "grunt-babel": "^6.0.0", 35 | "grunt-cli": "^0.1.13", 36 | "grunt-contrib-clean": "^0.7.0", 37 | "grunt-contrib-watch": "^0.6.1", 38 | "grunt-eslint": "^17.3.1", 39 | "grunt-mocha-test": "^0.12.7", 40 | "mocha": "^2.3.4", 41 | "react": "^0.14.7", 42 | "react-dom": "^0.14.7", 43 | "react-router": "^2.0.0", 44 | "rewrite": "0.0.1", 45 | "webpack": "^1.12.13", 46 | "webpack-dev-middleware": "^1.5.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/.eslintrc-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | mocha: true 4 | globals: 5 | assert: true 6 | expect: true 7 | extends: 8 | - ".eslintrc.yaml" 9 | -------------------------------------------------------------------------------- /src/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | browser: true 4 | extends: 5 | - "../eslint/eslint-defaults.yaml" 6 | - "../eslint/eslint-es2015.yaml" 7 | -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | import bindMethods from './bind-methods'; 2 | 3 | export default class Auth { 4 | constructor({ 5 | browserHistory, 6 | key = 'credentials', 7 | loginRoute = '/login', 8 | loggedInRoute = '/', 9 | authUrl, 10 | parseCredentials, 11 | }) { 12 | this.browserHistory = browserHistory; 13 | this.key = key; 14 | this.loginRoute = loginRoute; 15 | this.loggedInRoute = loggedInRoute; 16 | this.authUrl = authUrl; 17 | this.parseCredentials = parseCredentials; 18 | this.baseHref = null; 19 | // Simplify passing methods around. 20 | bindMethods(this); 21 | } 22 | 23 | // Store credentials for later retrieval. 24 | login(credentials) { 25 | if (!credentials) { 26 | throw new TypeError('Missing login credentials.'); 27 | } 28 | const json = JSON.stringify(credentials); 29 | localStorage.setItem(this.key, json); 30 | } 31 | 32 | // Remove stored credentials. If `redirectBack` is true, logging in will 33 | // redirect back to the current route instead of the default `loggedInRoute`. 34 | // (use in cases where the user gets automatically logged out) 35 | logout({redirectBack} = {}) { 36 | localStorage.removeItem(this.key); 37 | // Redirect to the login page. 38 | if (this.browserHistory) { 39 | const nextPathname = redirectBack ? this.getRoutePath() : this.loggedInRoute; 40 | this.redirectToLogin({nextPathname}); 41 | } 42 | } 43 | 44 | // Get stored credentials. If none are found, attempt to parse them from 45 | // the current environment, and then store them if found. If "exists" is 46 | // true, return a Boolean value based on whether or not they are stored. 47 | getCredentials({exists} = {}) { 48 | let json = localStorage.getItem(this.key); 49 | if (!json) { 50 | const credentials = this.parseCredentials(); 51 | if (credentials) { 52 | this.login(credentials); 53 | } 54 | json = localStorage.getItem(this.key); 55 | } 56 | return exists ? Boolean(json) : json ? JSON.parse(json) : {}; 57 | } 58 | 59 | // Do stored credentials exist? If none are found, attempt to parse them 60 | // from the current environment, and then store them if found. 61 | isLoggedIn() { 62 | return this.getCredentials({exists: true}); 63 | } 64 | 65 | // Pass in a location object, and get back path + querystring as a string. 66 | // Don't pass in anything to use the current page's location object. 67 | getRoutePath(_location) { 68 | let pathname, search; 69 | if (_location) { 70 | ({pathname, search} = _location); 71 | } 72 | else { 73 | ({pathname, search} = location); 74 | pathname = pathname.slice(this.getBasePath().length); 75 | } 76 | return `${pathname}${search}`; 77 | } 78 | 79 | // Get relative "base" path for the current app. 80 | getBasePath() { 81 | if (this.baseHref === null) { 82 | const elem = document.getElementsByTagName('base')[0]; 83 | const href = elem && elem.getAttribute('href') || ''; 84 | this.baseHref = href.replace(/\/$/, ''); 85 | } 86 | return this.baseHref; 87 | } 88 | 89 | // Get absolute "base" path for the current app. 90 | getBaseUrl(path) { 91 | if (!path) { 92 | path = this.loggedInRoute; 93 | } 94 | const baseHref = this.getBasePath(); 95 | return `${location.protocol}//${location.host}${baseHref}${path}`; 96 | } 97 | 98 | // Redirect to the login page. If `replace` is specified, that replace 99 | // function will be used, otherwise the specified `browserHistory` will be, 100 | // otherwise nothing will happen. `nextPathname` is the URL to redirect to 101 | // after successful login. 102 | redirectToLogin({replace, nextPathname}) { 103 | if (!replace) { 104 | replace = this.browserHistory && this.browserHistory.replace; 105 | } 106 | if (replace) { 107 | replace({ 108 | pathname: this.loginRoute, 109 | state: {nextPathname}, 110 | }); 111 | } 112 | } 113 | 114 | // React-router onEnter handler. If the given route (or any child route) is 115 | // not authed, redirect to the specified `loginRoute` 116 | requireAuth(nextState, replace) { 117 | if (!this.isLoggedIn()) { 118 | const nextPathname = this.getRoutePath(nextState.location); 119 | this.redirectToLogin({replace, nextPathname}); 120 | } 121 | } 122 | 123 | // React-router onEnter handler. If the given route (or any child route) is 124 | // authed, redirect to the specified `loggedInRoute` 125 | requireNoAuth(nextState, replace) { 126 | if (this.isLoggedIn()) { 127 | replace(this.loggedInRoute); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/auth.test.js: -------------------------------------------------------------------------------- 1 | import Auth from './auth'; 2 | 3 | describe('auth', function() { 4 | 5 | it('should export a constructor function', function() { 6 | expect(Auth).to.be.a('function'); 7 | }); 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /src/bind-methods.js: -------------------------------------------------------------------------------- 1 | export default function(obj, methodNames) { 2 | const protoObj = Object.getPrototypeOf(obj); 3 | 4 | let methods = methodNames; 5 | if (!methods) { 6 | const propNames = Object.getOwnPropertyNames(protoObj); 7 | methods = propNames.filter(name => typeof protoObj[name] === 'function'); 8 | } 9 | 10 | methods.forEach(name => { 11 | obj[name] = protoObj[name].bind(obj); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/bind-methods.test.js: -------------------------------------------------------------------------------- 1 | import bindMethods from './bind-methods'; 2 | 3 | describe('bindMethods', function() { 4 | 5 | beforeEach(function() { 6 | class Test { 7 | constructor(foo) { 8 | this.foo = foo; 9 | } 10 | bar(arg) { 11 | return this.foo + arg; 12 | } 13 | baz(arg) { 14 | return this.foo + arg; 15 | } 16 | } 17 | this.Test = Test; 18 | }); 19 | 20 | it('javascript 101', function() { 21 | const obj = new this.Test(100); 22 | expect(obj.bar(1)).to.equal(101); 23 | expect(obj.baz(2)).to.equal(102); 24 | 25 | const {bar, baz} = obj; 26 | const obj2 = {bar, baz, foo: 9000}; 27 | expect(obj2.bar(1)).to.equal(9001); 28 | expect(obj2.baz(2)).to.equal(9002); 29 | }); 30 | 31 | it('should bind all methods by default', function() { 32 | const obj = new this.Test(100); 33 | 34 | bindMethods(obj); 35 | 36 | const {bar, baz} = obj; 37 | const obj2 = {bar, baz, foo: 9000}; 38 | expect(obj2.bar(1)).to.equal(101); 39 | expect(obj2.baz(2)).to.equal(102); 40 | }); 41 | 42 | it('should bind only the specified methods', function() { 43 | const obj = new this.Test(100); 44 | 45 | bindMethods(obj, ['bar']); 46 | 47 | const {bar, baz} = obj; 48 | const obj2 = {bar, baz, foo: 9000}; 49 | expect(obj2.bar(1)).to.equal(101); 50 | expect(obj2.baz(2)).to.equal(9002); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './auth'; 2 | -------------------------------------------------------------------------------- /tools/.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "../.eslintrc.yaml" 4 | - "../eslint/eslint-es2015.yaml" 5 | 6 | -------------------------------------------------------------------------------- /tools/gruntfile.js: -------------------------------------------------------------------------------- 1 | import {grunt} from '../Gruntfile'; 2 | 3 | const babel = { 4 | options: { 5 | sourceMap: 'inline', 6 | plugins: ['transform-runtime'], 7 | }, 8 | build: { 9 | src: '**/*.js', 10 | expand: true, 11 | cwd: 'src', 12 | dest: 'build', 13 | }, 14 | }; 15 | 16 | const clean = { 17 | build: 'build', 18 | }; 19 | 20 | const eslint = { 21 | src: { 22 | options: {configFile: 'src/.eslintrc.yaml'}, 23 | src: ['src/**/*.js', '!<%= eslint.test.src %>'], 24 | }, 25 | test: { 26 | options: {configFile: 'src/.eslintrc-test.yaml'}, 27 | src: 'src/**/*.test.js', 28 | }, 29 | examples: { 30 | options: {configFile: 'examples/.eslintrc.yaml'}, 31 | src: 'examples/**/*.{js,jsx}', 32 | }, 33 | tools: { 34 | options: {configFile: 'tools/.eslintrc.yaml'}, 35 | src: 'tools/**/*.js', 36 | }, 37 | gruntfile: { 38 | options: {configFile: '.eslintrc.yaml'}, 39 | src: 'Gruntfile.js', 40 | }, 41 | }; 42 | 43 | const mochaTest = { 44 | test: { 45 | options: { 46 | reporter: 'spec', 47 | quiet: false, 48 | clearRequireCache: true, 49 | require: [ 50 | 'babel-register', 51 | 'tools/test-globals', 52 | ], 53 | }, 54 | src: '<%= eslint.test.src %>', 55 | }, 56 | }; 57 | 58 | const watch = { 59 | src: { 60 | files: ['<%= eslint.src.src %>'], 61 | tasks: ['eslint:src', 'mochaTest', 'build'], 62 | }, 63 | test: { 64 | files: ['<%= eslint.test.src %>'], 65 | tasks: ['eslint:test', 'mochaTest'], 66 | }, 67 | tools: { 68 | options: {reload: true}, 69 | files: ['<%= eslint.tools.src %>'], 70 | tasks: ['eslint:tools'], 71 | }, 72 | gruntfile: { 73 | options: {reload: true}, 74 | files: ['<%= eslint.gruntfile.src %>'], 75 | tasks: ['eslint:gruntfile'], 76 | }, 77 | lint: { 78 | options: {reload: true}, 79 | files: ['.eslintrc*', 'eslint/*'], 80 | tasks: ['eslint'], 81 | }, 82 | }; 83 | 84 | grunt.initConfig({ 85 | babel, 86 | clean, 87 | eslint, 88 | mochaTest, 89 | watch, 90 | }); 91 | 92 | grunt.registerTask('build', ['clean', 'babel']); 93 | grunt.registerTask('test', ['eslint', 'mochaTest']); 94 | grunt.registerTask('default', ['watch']); 95 | 96 | grunt.loadNpmTasks('grunt-babel'); 97 | grunt.loadNpmTasks('grunt-contrib-clean'); 98 | grunt.loadNpmTasks('grunt-contrib-watch'); 99 | grunt.loadNpmTasks('grunt-eslint'); 100 | grunt.loadNpmTasks('grunt-mocha-test'); 101 | -------------------------------------------------------------------------------- /tools/test-globals.js: -------------------------------------------------------------------------------- 1 | import {assert, expect} from 'chai'; 2 | 3 | global.assert = assert; 4 | global.expect = expect; 5 | --------------------------------------------------------------------------------