├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── docs └── configuration.md ├── internals └── test.setup.js ├── package.json └── src ├── components ├── Auth.jsx ├── Auth.spec.jsx ├── Facebook.jsx ├── Facebook.spec.jsx ├── Google.jsx ├── Google.spec.jsx ├── OAuth2.jsx └── PopupButton.jsx ├── index.js ├── index.spec.js ├── internals ├── config.js ├── defaults.js ├── storage │ ├── component.jsx │ ├── index.js │ ├── index.spec.js │ ├── storage.js │ └── storage.spec.js ├── utils.js └── utils.spec.js ├── local.js ├── shared.js └── user.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ], 6 | "env": { 7 | "cjs": { 8 | "plugins": ["add-module-exports"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = tab 8 | tab_width = 2 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | max_line_length = 80 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.json] 17 | indent_style = space 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | npm-debug.log 4 | .DS_Store 5 | coverage 6 | .nyc_output 7 | .coveralls.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | script: 5 | - npm test 6 | after_success: 7 | - npm run test:coveralls 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React JWT Auth 2 | 3 | ### Token based authentication for react. 4 | 5 | [![Build Status](https://travis-ci.org/fullstackforger/react-jwt-auth.svg?branch=master)](https://travis-ci.org/fullstackforger/react-jwt-auth) 6 | [![Coverage Status](https://coveralls.io/repos/github/fullstackforger/react-jwt-auth/badge.svg?branch=master)](https://coveralls.io/github/fullstackforger/react-jwt-auth?branch=master) 7 | [![Code Climate](https://codeclimate.com/github/fullstackforger/react-jwt-auth/badges/gpa.svg)](https://codeclimate.com/github/fullstackforger/react-jwt-auth) 8 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/4d2136784d334d7f8f4178c7b979d9f2)](https://www.codacy.com/app/fullstackforger/react-jwt-auth?utm_source=github.com&utm_medium=referral&utm_content=fullstackforger/react-jwt-auth&utm_campaign=Badge_Grade) 9 | [![npm](https://img.shields.io/npm/v/react-jwt-auth.svg)](https://www.npmjs.com/package/react-jwt-auth) 10 | [![dependencies Status](https://david-dm.org/fullstackforger/react-jwt-auth/status.svg)](https://david-dm.org/fullstackforger/react-jwt-auth) 11 | [![devDependencies Status](https://david-dm.org/fullstackforger/react-jwt-auth/dev-status.svg)](https://david-dm.org/fullstackforger/react-jwt-auth?type=dev) 12 | [![peerDependencies Status](https://david-dm.org/fullstackforger/react-jwt-auth/peer-status.svg)](https://david-dm.org/fullstackforger/react-jwt-auth?type=peer) 13 | 14 | > **Work in progress. Contributions are welcomed!** 15 | 16 | ## Inspiration 17 | 18 | Satellizer, for its simplicity, clear code and maturity was initial source of inspiration. 19 | As there is no need to reinvent the wheel, some of Satellizer code has been reused where possible and adopted to React. 20 | 21 | ## Setup 22 | 23 | Before you start using component you should configure it, eg: 24 | ``` 25 | ReactDOM.render( 26 | 27 | 28 | , 29 | document.getElementById('app') 30 | ); 31 | ``` 32 | 33 | More in docs, [configuration](./docs/configuration.md) section. 34 | 35 | ## Social buttons 36 | 37 | Here is simplest example of social buttons in use inside of your component `reder()` method. 38 | 39 | ``` 40 | render () { 41 | return ( 42 |
43 | 44 | 45 |
46 | ) 47 | } 48 | ``` 49 | 50 | ## Testing 51 | 52 | You can run all tests with: 53 | ``` 54 | npm test 55 | ``` 56 | 57 | ### Test coverage 58 | 59 | We run test coverage with [nyc](https://www.npmjs.com/package/nyc) and [here][nyc-why] is why. 60 | [nyc-why]: http://stackoverflow.com/a/33725069/6096446) 61 | 62 | You can run test coverage task locally with: 63 | ``` 64 | npm run test:coverage 65 | ``` 66 | It will: 67 | * run all tests 68 | * generate coverage data 69 | * create coverage report files in `./coverage` folder 70 | * check minimum coverage requirements set to 95% 71 | 72 | Additionally we use [coveralls.io](https://coveralls.io/) for coverage badge generation. 73 | [![Coverage Status](https://coveralls.io/repos/github/fullstackforger/react-jwt-auth/badge.svg?branch=master)](https://coveralls.io/github/fullstackforger/react-jwt-auth?branch=master) 74 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Initial setup 4 | 5 | Before you start using *React JWT Auth* module you need point it to your REST URL server. 6 | You can do it passing `baseUrl` param into `Auth` component. 7 | 8 | ``` 9 | 10 | ``` 11 | 12 | ### Wrapping all components 13 | 14 | Easiest method is to wrap your custom components with Auth component. 15 | 16 | ``` 17 | ReactDOM.render( 18 | 19 | 20 | , 21 | document.getElementById('app') 22 | ) 23 | ``` 24 | 25 | ### Wrapping individual components 26 | 27 | Alternatively you can wrap only components which require authentication. 28 | 29 | ``` 30 | ReactDOM.render( 31 |
32 | 33 | 34 | 35 | 36 | 37 |
, 38 | document.getElementById('app') 39 | ) 40 | ``` 41 | 42 | ### No wrapping 43 | 44 | Primary role of `Auth` component is setting up initial configuration, therefore you don't have to wrap. 45 | Below code will also work. 46 | 47 | ``` 48 | ReactDOM.render( 49 |
50 | 51 | 52 |
, 53 | document.getElementById('app') 54 | ) 55 | ``` 56 | 57 | ## Default configuration 58 | 59 | Here is the list of all configuration options. 60 | 61 | ``` 62 | const authConfig = { 63 | // --- DEFAULTS --- 64 | // tokenName: 'if-token', 65 | // authHeader: 'Authorization', 66 | // authToken: 'Bearer', 67 | // baseUrl: '/', 68 | // loginUrl: 'auth/login', 69 | // signupUrl: 'auth/signup', 70 | // refreshUrl: 'auth/refresh', 71 | // oauthUrl: 'auth/{provider}', // dynamic 72 | // profileUrl: 'me' 73 | // --- REQUIRED --- 74 | baseUrl="http://localhost:8080/api/" 75 | } 76 | 77 | ReactDOM.render( 78 | 79 | 80 | , 81 | document.getElementById('app') 82 | ) 83 | ``` -------------------------------------------------------------------------------- /internals/test.setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom' 2 | import atob from 'atob' 3 | 4 | global.document = jsdom.jsdom('') 5 | global.window = document.defaultView 6 | global.navigator = { userAgent: 'node.js' } 7 | 8 | global.atob = atob 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-jwt-auth", 3 | "version": "0.6.0", 4 | "devEngines": { 5 | "node": "4.x || 5.x || 6.x", 6 | "npm": "2.x || 3.x" 7 | }, 8 | "description": "JSON Web Token authentication and authorization component for React", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fullstackforger/react-jwt-auth" 12 | }, 13 | "files": [ 14 | "package.json", 15 | "lib", 16 | "README" 17 | ], 18 | "main": "lib/cjs/index", 19 | "jsnext:main": "lib/es6/index", 20 | "scripts": { 21 | "build": "npm run build:cjs && npm run build:es", 22 | "build:cjs": "rimraf lib && cross-env BABEL_ENV=cjs && babel ./src -d lib/cjs --ignore **/*.spec.*", 23 | "build:es": "rimraf es6 && cross-env BABEL_ENV=es babel ./src -d lib/es6 --ignore **/*.spec.*", 24 | "prebuild": "npm test", 25 | "prepublish": "npm run build", 26 | "preversion": "npm test", 27 | "postversion": "git push && git push --tags", 28 | "test": "npm run test:mocha", 29 | "test:clean": "rimraf coverage .nyc_output", 30 | "test:mocha": "mocha ./internals/test.setup.js ./src/**/*.spec.js ./src/**/*.spec.jsx --require babel-core/register", 31 | "test:watch": "npm run test:mocha -- --watch --reporter=min", 32 | "test:cover": "npm run test:clean && nyc npm run test:mocha -- --reporter=dot", 33 | "test:cover:check": "nyc check-coverage --lines 95 --functions 95 --branches 95", 34 | "test:cover:report": "nyc report --reporter=lcov", 35 | "test:coverage": "npm run test:cover && npm run test:cover:report && npm run test:cover:check", 36 | "test:coveralls": "npm run test:cover && nyc report --reporter=text-lcov | coveralls" 37 | }, 38 | "keywords": [ 39 | "react", 40 | "auth", 41 | "jwt", 42 | "authentication", 43 | "token", 44 | "component" 45 | ], 46 | "author": "", 47 | "license": "MIT", 48 | "dependencies": { 49 | "enverse": "^0.2.3" 50 | }, 51 | "peerDependencies": { 52 | "react": "^15.3.2" 53 | }, 54 | "devDependencies": { 55 | "atob": "^2.0.3", 56 | "babel-cli": "^6.16.0", 57 | "babel-core": "^6.17.0", 58 | "babel-preset-es2015": "^6.18.0", 59 | "babel-preset-react": "^6.16.0", 60 | "chai": "^3.5.0", 61 | "coveralls": "^2.11.14", 62 | "cross-env": "^3.1.3", 63 | "enzyme": "^2.5.1", 64 | "fetch-mock": "^5.5.0", 65 | "istanbul": "^0.4.5", 66 | "jsdom": "^9.8.0", 67 | "mocha": "^3.1.2", 68 | "nuc": "^0.3.2", 69 | "nyc": "^8.3.2", 70 | "react": "^15.3.2", 71 | "react-addons-test-utils": "^15.3.2", 72 | "react-dom": "^15.3.2", 73 | "rimraf": "^2.5.4", 74 | "sinon": "^1.17.6" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import config from '../internals/config' 5 | import { preventBadFacebookHash } from '../internals/utils' 6 | 7 | export default class Auth extends Component { 8 | constructor (props) { 9 | super(props) 10 | config.assign(props) 11 | } 12 | 13 | componentWillMount() { 14 | preventBadFacebookHash() 15 | } 16 | render() { 17 | return this.props.children || null 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/Auth.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import sinon from 'sinon' 3 | import { expect } from 'chai' 4 | import { mount, shallow } from 'enzyme' 5 | 6 | import Auth from './Auth' 7 | 8 | describe('Auth', () => { 9 | 10 | it('should mount', () => { 11 | const wrapper = shallow() 12 | expect(wrapper).to.not.be.an('undefined') 13 | }) 14 | 15 | it('should call componentWillMount', () => { 16 | sinon.spy(Auth.prototype, 'componentWillMount') 17 | const wrapper = mount() 18 | expect(Auth.prototype.componentWillMount.calledOnce).to.equal(true) 19 | }) 20 | 21 | it('should wrap children', () => { 22 | const wrapper = shallow( 23 | 24 |
25 | 26 | ) 27 | expect(wrapper.contains(
)).to.equal(true) 28 | }) 29 | }) -------------------------------------------------------------------------------- /src/components/Facebook.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import OAuth2 from './OAuth2' 4 | import env from 'enverse' 5 | import { isClient } from '../internals/utils' 6 | 7 | const defaultProps = { 8 | clientId: null, 9 | name: 'facebook', 10 | label: 'Sign in with Facebook', 11 | tokenEndpoint: '/auth/facebook', 12 | oauthProvider: 'facebook', 13 | oauthEndpoint: 'https://www.facebook.com/v2.5/dialog/oauth', 14 | redirectUri: env.is.browser ? window.location.origin + '/' : undefined, // FB requires be followed by trailing slash for FB 15 | requiredUrlParams: ['display', 'scope'], 16 | scope: ['email'], 17 | scopeDelimiter: ',', 18 | display: 'popup', 19 | oauthType: '2.0', 20 | popupOptions: { width: 580, height: 400 }, 21 | style: { 22 | color: '#fff', 23 | backgroundColor: '#3b5998', 24 | border: '1px solid #335190', 25 | padding: '5px 15px' 26 | }, 27 | className: 'btn btn-md' 28 | } 29 | 30 | export default class Facebook extends Component { 31 | 32 | render () { 33 | return ( 34 | 35 | {this.props.children || null} 36 | 37 | ) 38 | } 39 | } 40 | 41 | Facebook.defaultProps = defaultProps 42 | Facebook.propTypes = OAuth2.propTypes 43 | -------------------------------------------------------------------------------- /src/components/Facebook.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import sinon from 'sinon' 3 | import { expect } from 'chai' 4 | import { mount, shallow } from 'enzyme' 5 | 6 | describe('Facebook', () => { 7 | let Facebook 8 | 9 | describe('on the client', () => { 10 | before(() => { 11 | delete require.cache[require.resolve('./Facebook')] 12 | Facebook = require('./Facebook').default 13 | }) 14 | 15 | runSuite() 16 | }) 17 | 18 | describe('on the server', () => { 19 | let windowBckp 20 | 21 | before(() => { 22 | windowBckp = global.window 23 | delete global.window 24 | delete require.cache[require.resolve('./Facebook')] 25 | Facebook = require('./Facebook').default 26 | }) 27 | 28 | after(() => { 29 | global.window = windowBckp 30 | }) 31 | 32 | runSuite() 33 | }) 34 | 35 | function runSuite() { 36 | it('should mount', () => { 37 | const wrapper = mount() 38 | expect(wrapper).to.not.be.an('undefined') 39 | }) 40 | 41 | it('should wrap children', () => { 42 | const wrapper = shallow( 43 | 44 |
45 | 46 | ) 47 | expect(wrapper.contains(
)).to.equal(true) 48 | }) 49 | } 50 | }) 51 | 52 | -------------------------------------------------------------------------------- /src/components/Google.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import OAuth2 from './OAuth2' 4 | import env from 'enverse' 5 | import { isClient } from '../internals/utils' 6 | 7 | const defaultProps = { 8 | clientId: null, 9 | name: 'google', 10 | label: 'Sign in with Google', 11 | tokenEndpoint: '/auth/google', 12 | oauthProvider: 'google', 13 | oauthEndpoint: 'https://accounts.google.com/o/oauth2/auth', 14 | redirectUri: env.is.browser ? window.location.origin : undefined, 15 | requiredUrlParams: ['scope'], 16 | optionalUrlParams: ['display', 'state'], 17 | scope: ['profile', 'email'], 18 | scopePrefix: 'openid', 19 | scopeDelimiter: ' ', 20 | display: 'popup', 21 | oauthType: '2.0', 22 | popupOptions: { width: 452, height: 533 }, 23 | style: { 24 | color: '#fff', 25 | backgroundColor: '#dd4b39', 26 | border: '1px solid #d54331', 27 | padding: '5px 15px' 28 | }, 29 | state: () => encodeURIComponent(Math.random().toString(36).substr(2)) 30 | } 31 | 32 | export default class Google extends Component { 33 | 34 | render () { 35 | return ( 36 | 37 | {this.props.children || null} 38 | 39 | ) 40 | } 41 | } 42 | 43 | Google.defaultProps = defaultProps 44 | Google.propTypes = OAuth2.propTypes 45 | -------------------------------------------------------------------------------- /src/components/Google.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import sinon from 'sinon' 3 | import { expect } from 'chai' 4 | import { mount, shallow } from 'enzyme' 5 | 6 | describe('Google', () => { 7 | let Google 8 | 9 | describe('on the client', () => { 10 | before(() => { 11 | delete require.cache[require.resolve('./Google')] 12 | Google = require('./Google').default 13 | }) 14 | 15 | runSuite() 16 | }) 17 | 18 | describe('on the server', () => { 19 | let windowBckp 20 | 21 | before(() => { 22 | windowBckp = global.window 23 | delete global.window 24 | delete require.cache[require.resolve('./Google')] 25 | Google = require('./Google').default 26 | }) 27 | 28 | after(() => { 29 | global.window = windowBckp 30 | }) 31 | 32 | runSuite() 33 | }) 34 | 35 | function runSuite() { 36 | it('should mount', () => { 37 | const wrapper = mount() 38 | expect(wrapper).to.not.be.an('undefined') 39 | }) 40 | 41 | it('should wrap children', () => { 42 | const wrapper = shallow( 43 | 44 |
45 | 46 | ) 47 | expect(wrapper.contains(
)).to.equal(true) 48 | }) 49 | } 50 | }) 51 | 52 | -------------------------------------------------------------------------------- /src/components/OAuth2.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import storage from '../internals/storage' 5 | import PopupButton from './PopupButton' 6 | import { exchangeCodeForToken } from '../local' 7 | 8 | const propTypes = { 9 | name: PropTypes.string.isRequired, 10 | label: PropTypes.string, 11 | clientId: PropTypes.string.isRequired, 12 | onSignIn: PropTypes.func, 13 | onSignInSuccess: PropTypes.func, 14 | onSignInFailed: PropTypes.func, 15 | tokenEndpoint: PropTypes.string.isRequired, 16 | oauthProvider: PropTypes.string.isRequired, 17 | oauthEndpoint: PropTypes.string.isRequired, 18 | redirectUri: PropTypes.string, 19 | scope: PropTypes.arrayOf(PropTypes.string), 20 | scopePrefix: PropTypes.string, 21 | scopeDelimiter: PropTypes.string, 22 | state: PropTypes.oneOfType([ 23 | PropTypes.string, 24 | PropTypes.func 25 | ]), 26 | requiredUrlParams: PropTypes.arrayOf(PropTypes.string), 27 | defaultUrlParams: PropTypes.arrayOf(PropTypes.string), 28 | responseType: PropTypes.string, 29 | responseParams: PropTypes.arrayOf(PropTypes.string), 30 | oauthType: PropTypes.string, 31 | popupOptions: PropTypes.shape({ 32 | width: PropTypes.number, 33 | height: PropTypes.number 34 | }), 35 | style: PropTypes.object, 36 | polling: PropTypes.bool 37 | } 38 | 39 | const defaultProps = { 40 | defaultUrlParams: ['response_type', 'client_id', 'redirect_uri'], 41 | responseType: 'code', 42 | responseParams: ['code', 'clientId', 'redirectUri'], 43 | oauthType: '2.0', 44 | style: {}, 45 | popupOptions: { width: 500, height: 500 }, 46 | polling: true 47 | } 48 | 49 | export default class OAuth2 extends Component { 50 | constructor(props) { 51 | super(props) 52 | //const { name, state, popupOptions, redirectUri, responseType } = params 53 | this.onClick = this.onClick.bind(this) 54 | this.onClose = this.onClose.bind(this) 55 | } 56 | 57 | buildQueryString() { 58 | const props = this.props 59 | const urlParamsCategories = ['defaultUrlParams', 'requiredUrlParams', 'optionalUrlParams']; 60 | const keyValuePairs = []; 61 | 62 | urlParamsCategories.forEach((paramsCategory) => { 63 | 64 | if (!props[paramsCategory] || !props[paramsCategory].forEach) { 65 | return 66 | } 67 | 68 | props[paramsCategory].forEach((paramName) => { 69 | 70 | let paramValue = typeof(props[paramName]) === 'function' 71 | ? props[paramName]() 72 | : props[OAuth2.camelCase(paramName)] 73 | 74 | if (paramName === 'redirect_uri' && !paramValue) { 75 | return 76 | } 77 | 78 | if (paramName === 'state') { 79 | const stateName = props.name + '_state' 80 | paramValue = encodeURIComponent(storage.get(stateName)) 81 | } 82 | 83 | if (paramName === 'scope' && Array.isArray(paramValue)) { 84 | paramValue = paramValue.join(props.scopeDelimiter) 85 | if (props.scopePrefix) { 86 | paramValue = [props.scopePrefix, paramValue].join(props.scopeDelimiter) 87 | } 88 | } 89 | 90 | keyValuePairs.push([paramName, paramValue]) 91 | }) 92 | }) 93 | 94 | return keyValuePairs.map(pair => pair.join('=')).join('&') 95 | } 96 | 97 | onClick() { 98 | if(this.onSignIn) { 99 | this.onSignIn(Object.assign({}, this.props)) 100 | } 101 | } 102 | 103 | onClose(queryStringData) { 104 | if (!queryStringData.error) { 105 | const oauthData = {} 106 | const provider = this.props.oauthProvider 107 | this.props.responseParams.forEach(prop => { 108 | switch(prop) { 109 | case 'code': 110 | oauthData[prop] = queryStringData.code 111 | break 112 | case 'clientId': 113 | case 'redirectUri': 114 | oauthData[prop] = this.props[prop] 115 | break 116 | default: 117 | oauthData[prop] = queryStringData[key]; 118 | } 119 | }) 120 | 121 | exchangeCodeForToken(provider, oauthData).then((token) => { 122 | if (this.props.onSignInSuccess) { 123 | this.props.onSignInSuccess({token}) 124 | } 125 | }).catch((error) => { 126 | if (this.props.onSignInFailed) { 127 | this.props.onSignInFailed(error) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | render() { 134 | const props = this.props 135 | const popupProps = { 136 | label: props.label, 137 | width: props.width, 138 | height: props.height, 139 | popupUrl: [props.oauthEndpoint, this.buildQueryString()].join('?'), 140 | redirectUri: props.redirectUri, // todo: remove coupling with popup 141 | polling: props.polling, 142 | onClick: this.onClick, 143 | onClose: this.onClose, 144 | style: props.style, 145 | className: props.className 146 | } 147 | 148 | return ( 149 | 150 | { this.props.children || null } 151 | 152 | ) 153 | } 154 | } 155 | 156 | OAuth2.camelCase = (name) => { 157 | return name.replace(/([\:\-\_]+(.))/g, (_, separator, letter, offset) => { 158 | return offset ? letter.toUpperCase() : letter; 159 | }) 160 | } 161 | 162 | OAuth2.propTypes = propTypes 163 | OAuth2.defaultProps = defaultProps 164 | -------------------------------------------------------------------------------- /src/components/PopupButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { parseQueryString } from '../internals/utils' 4 | 5 | const propTypes = { 6 | style: PropTypes.object, 7 | width: PropTypes.number, 8 | height: PropTypes.number, 9 | popupUrl: PropTypes.string.isRequired, 10 | autoClose: PropTypes.bool, 11 | autoCloseUri: PropTypes.string, 12 | onClick: PropTypes.func, 13 | onClose: PropTypes.func, 14 | polling: PropTypes.bool 15 | } 16 | 17 | const defaultProps = { 18 | style: {}, 19 | width: 500, 20 | height: 500, 21 | polling: true, 22 | autoClose: true, 23 | } 24 | 25 | class PopupButton extends React.Component { 26 | constructor(props) { 27 | super(props) 28 | 29 | this.state = { 30 | open: false 31 | } 32 | 33 | this.onClick = this.onClick.bind(this) 34 | } 35 | 36 | componentDidUpdate() { 37 | if (this.state.open) { 38 | this.open() 39 | } 40 | } 41 | 42 | onClick() { 43 | this.setState({ open: true }) 44 | if (this.props.onClick) { 45 | this.props.onClick() 46 | } 47 | } 48 | 49 | onClose(queryStringData) { 50 | this.setState({ open: false }) 51 | if (this.props.onClose) { 52 | this.props.onClose(queryStringData) 53 | } 54 | } 55 | 56 | open() { 57 | let { height, width } = this.props 58 | let options = { 59 | width: width, 60 | height: height, 61 | top: window.screenY + ((window.outerHeight - height) / 2.5), 62 | left: window.screenX + ((window.outerWidth - width) / 2), 63 | resizable: 0, // IE only 64 | scrollbars: 0 // IE, Firefox & Opera only 65 | } 66 | 67 | const popup = window.open(this.props.popupUrl, '_blank', PopupButton.generateSpec(options)) 68 | popup.focus() 69 | 70 | if (this.props.popupUrl === 'about:blank') { 71 | popup.document.body.innerHTML = 'Loading...' 72 | } 73 | 74 | if (this.props.polling) { 75 | this.pollPopup(popup) 76 | } 77 | } 78 | 79 | pollPopup(window) { 80 | const autoCloseUriPath = !this.props.autoCloseUri 81 | ? document.location.origin + document.location.pathname 82 | : this.props.autoCloseUri 83 | 84 | let queryStringData = {} 85 | let closing = false 86 | 87 | const polling = setInterval(() => { 88 | if (!window || window.closed || closing) { 89 | clearInterval(polling) 90 | if (queryStringData.error) { 91 | console.error(queryStringData.error) 92 | } 93 | this.onClose(queryStringData) 94 | window.close() 95 | } 96 | try { 97 | const popupUrlPath = window.location.origin + window.location.pathname 98 | 99 | // todo: decouple, use handler to this outside 100 | if (popupUrlPath === autoCloseUriPath) { 101 | if (window.location.search || window.location.hash) { 102 | const query = parseQueryString(window.location.search.substring(1).replace(/\/$/, '')) 103 | const hash = parseQueryString(window.location.hash.substring(1).replace(/[\/$]/, '')) 104 | queryStringData = Object.assign({}, query, hash) 105 | closing = this.props.autoClose 106 | } else { 107 | console.info('OAuth redirect has occurred but no query or hash parameters were found.') 108 | } 109 | } 110 | } catch (error) { 111 | // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame. 112 | // A hack to get around same-origin security policy errors in Internet Explorer. 113 | } 114 | }, 250) 115 | } 116 | 117 | renderInternalElement() { 118 | if (this.props.children instanceof Array) { 119 | return this.props.children 120 | } 121 | return React.cloneElement(this.props.children, { 122 | onClick: this.onClick 123 | }) 124 | } 125 | 126 | render() { 127 | return this.props.children 128 | ? this.renderInternalElement(this.props) 129 | : 133 | } 134 | } 135 | 136 | PopupButton.generateSpec = (options) => { 137 | return Object.keys(options).reduce((previous, current, index) => { 138 | let final = index == 1 ? previous + '=' + options[previous] : previous 139 | return final + ',' + current + '=' + options[current] 140 | }) 141 | } 142 | 143 | PopupButton.defaultProps = defaultProps 144 | PopupButton.propTypes = propTypes 145 | 146 | export default PopupButton 147 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { isAuthenticated, getToken, setToken, getAuthHeader } from './shared' 2 | import { login, logout, signup, refreshToken } from './local' 3 | import { getProfile, updateProfile } from './user' 4 | 5 | import Facebook from './components/Facebook' 6 | export { Facebook } 7 | import Google from './components/Google' 8 | export { Google } 9 | 10 | import Auth from './components/Auth' 11 | export {Auth} 12 | 13 | export default { 14 | Auth, 15 | Facebook, 16 | Google, 17 | isAuthenticated, 18 | login, 19 | logout, 20 | signup, 21 | getToken, 22 | setToken, 23 | refreshToken, 24 | getAuthHeader, 25 | getProfile, 26 | updateProfile 27 | } -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import jwtAuth from './index' 3 | import {Auth, Google, Facebook} from './index' 4 | 5 | const expect = chai.expect 6 | const assert = chai.assert 7 | 8 | describe('react-jwt-auth', () => { 9 | 10 | it('should exist', () => { 11 | expect(jwtAuth).to.exist 12 | }) 13 | 14 | 15 | 16 | describe('components', () => { 17 | it('should expose Auth', () => { 18 | assert.isFunction(Auth) 19 | assert.isFunction(jwtAuth.Auth) 20 | }) 21 | 22 | it('should expose Facebook', () => { 23 | assert.isFunction(Facebook) 24 | assert.isFunction(jwtAuth.Facebook) 25 | }) 26 | 27 | it('should expose Google', () => { 28 | assert.isFunction(Google) 29 | assert.isFunction(jwtAuth.Google) 30 | }) 31 | }) 32 | 33 | describe('api methods', () => { 34 | 35 | it('should expose isAuthenticated', () => { 36 | assert.isFunction(jwtAuth.isAuthenticated) 37 | }) 38 | 39 | it('should expose login', () => { 40 | assert.isFunction(jwtAuth.login) 41 | }) 42 | 43 | it('should expose logout', () => { 44 | assert.isFunction(jwtAuth.logout) 45 | }) 46 | 47 | it('should expose signup', () => { 48 | assert.isFunction(jwtAuth.signup) 49 | }) 50 | 51 | it('should expose getToken', () => { 52 | assert.isFunction(jwtAuth.getToken) 53 | }) 54 | 55 | it('should expose setToken', () => { 56 | assert.isFunction(jwtAuth.setToken) 57 | }) 58 | 59 | it('should expose refreshToken', () => { 60 | assert.isFunction(jwtAuth.refreshToken) 61 | }) 62 | 63 | it('should expose getAuthHeader', () => { 64 | assert.isFunction(jwtAuth.getAuthHeader) 65 | }) 66 | 67 | it('should expose getProfile', () => { 68 | assert.isFunction(jwtAuth.getProfile) 69 | }) 70 | 71 | it('should expose updateProfile', () => { 72 | assert.isFunction(jwtAuth.updateProfile) 73 | }) 74 | }) 75 | 76 | 77 | }) -------------------------------------------------------------------------------- /src/internals/config.js: -------------------------------------------------------------------------------- 1 | import { configOpts } from './defaults' 2 | 3 | class Config { 4 | constructor(opts = configOpts) { 5 | this.assign(opts) 6 | } 7 | } 8 | 9 | Config.prototype.defaults = configOpts 10 | 11 | Config.prototype.assign = function (opts) { 12 | let config = this 13 | Object.keys(configOpts).map((key) => { 14 | config[key] = opts.hasOwnProperty(key) 15 | ? opts[key] 16 | : configOpts[key] 17 | }) 18 | } 19 | 20 | export default new Config () -------------------------------------------------------------------------------- /src/internals/defaults.js: -------------------------------------------------------------------------------- 1 | export const configOpts = { 2 | tokenName: 'if-token', 3 | authHeader: 'Authorization', 4 | authToken: 'Bearer', 5 | baseUrl: '/', 6 | loginUrl: 'auth/login', 7 | signupUrl: 'auth/signup', 8 | refreshUrl: 'auth/refresh', 9 | oauthUrl: 'auth/{provider}', // dynamic 10 | profileUrl: 'me' 11 | } 12 | 13 | export const fetchOpts = { 14 | method: 'GET', 15 | headers: { 16 | 'Accept': 'application/json', 17 | 'Content-Type': 'application/json' 18 | } 19 | } -------------------------------------------------------------------------------- /src/internals/storage/component.jsx: -------------------------------------------------------------------------------- 1 | // Storage Component for React 2 | // ------------------------------------------------------------------ 3 | // It stores data in `localStorage`, `sessionStorage` or caches it 4 | // Additionally it makes data available between different components 5 | // 6 | // Inspired by: 7 | // - https://github.com/yuanyan/react-storage 8 | // - https://github.com/sahat/satellizer/blob/master/src/storage.ts 9 | // ------------------------------------------------------------------ 10 | 11 | import React from 'react' 12 | import PropTypes from 'prop-types' 13 | import Storage from './storage' 14 | 15 | export default class StorageComponent extends React.Component { 16 | constructor(props) { 17 | super(props) 18 | } 19 | 20 | componentWillUpdate() { 21 | if(this.props.autoSave) { 22 | this.save(); 23 | this.storage = new Storage() 24 | } 25 | } 26 | 27 | save() { 28 | var value = this.props.useRaw 29 | ? this.props.value 30 | : JSON.stringify(this.props.value); 31 | 32 | Storage.set(this.props.name, value); 33 | } 34 | 35 | render() { 36 | return `[property value for ${this.props.name}]` 37 | } 38 | } 39 | 40 | Storage.propTypes = { 41 | name: PropTypes.string.isRequired, 42 | value: PropTypes.oneOfType([ 43 | PropTypes.string, 44 | PropTypes.object, 45 | PropTypes.array 46 | ]), 47 | useRaw: PropTypes.bool, 48 | autoSave: PropTypes.bool, 49 | cache: PropTypes.object, 50 | storage: PropTypes.object 51 | }, 52 | 53 | Storage.defaultProps = DataStore.defaults 54 | 55 | Storage.get = (key) => (this.storage.get(key)) 56 | 57 | Storage.set = (key, value) => (this.storage.remove(key, value)) 58 | 59 | Storage.remove = (key) => (this.storage.remove(key)) -------------------------------------------------------------------------------- /src/internals/storage/index.js: -------------------------------------------------------------------------------- 1 | import Storage from './storage' 2 | 3 | export default new Storage() -------------------------------------------------------------------------------- /src/internals/storage/index.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import jwtAuth from './index' 3 | 4 | const expect = chai.expect 5 | const assert = chai.assert 6 | 7 | describe('storage', () => { 8 | 9 | describe('on the client', () => { 10 | before(() => { 11 | delete require.cache[require.resolve('./index.js')] 12 | }) 13 | 14 | it('is accessible', () => { 15 | expect(require('./index')).to.exist 16 | }) 17 | }) 18 | 19 | describe('on the server', () => { 20 | 21 | let windowBckp 22 | before(() => { 23 | windowBckp = global.window 24 | delete global.window 25 | delete require.cache[require.resolve('./index.js')] 26 | }) 27 | 28 | after(() => { 29 | global.window = windowBckp 30 | }) 31 | 32 | it('is accessible', () => { 33 | expect(require('./index')).to.exist 34 | }) 35 | }) 36 | 37 | }) -------------------------------------------------------------------------------- /src/internals/storage/storage.js: -------------------------------------------------------------------------------- 1 | const cache = {} 2 | 3 | export default class Storage { 4 | constructor(opts = {}) { 5 | Object.keys(Storage.defaults).map((key) => { 6 | this[key] = opts.hasOwnProperty(key) 7 | ? opts[key] 8 | : Storage.defaults[key] 9 | }) 10 | } 11 | } 12 | 13 | Storage.defaults = { 14 | storage: typeof window !== 'undefined' ? window.localStorage : null, // localStorage, sessionStorage or custom conforming to storage API 15 | } 16 | 17 | Storage.create = (opts) => (new Storage(opts)) 18 | 19 | Storage.cache = cache 20 | 21 | Storage.prototype.cache = cache 22 | 23 | Storage.prototype.get = function (key) { 24 | try { 25 | return this.storage.getItem(key) 26 | } catch (e) { 27 | return this.cache[key] 28 | } 29 | } 30 | 31 | Storage.prototype.set = function (key, value) { 32 | try { 33 | this.storage.setItem(key, value) 34 | return value 35 | } catch (e) { 36 | return this.cache[key] = value 37 | } 38 | } 39 | 40 | Storage.prototype.remove = function (key) { 41 | try { 42 | this.storage.removeItem(key) 43 | } catch (e) { 44 | delete this.cache[key] 45 | } 46 | } -------------------------------------------------------------------------------- /src/internals/storage/storage.spec.js: -------------------------------------------------------------------------------- 1 | import jwtAuth from './index' 2 | import { expect } from 'chai' 3 | 4 | describe('Storage', () => { 5 | let Storage 6 | 7 | describe('on the client', () => { 8 | before(() => { 9 | delete require.cache[require.resolve('./storage.js')] 10 | Storage = require('./storage').default 11 | }) 12 | runSuite() 13 | }) 14 | 15 | describe('on the server', () => { 16 | let windowBckp 17 | 18 | before(() => { 19 | windowBckp = global.window 20 | delete global.window 21 | delete require.cache[require.resolve('./storage.js')] 22 | Storage = require('./storage').default 23 | }) 24 | 25 | after(() => { 26 | global.window = windowBckp 27 | }) 28 | 29 | runSuite() 30 | }) 31 | 32 | function runSuite () { 33 | 34 | it('is a constructor method', () => { 35 | const storage = new Storage() 36 | expect(storage).to.be.an.instanceof(Storage) 37 | }) 38 | 39 | describe('set()', () => { 40 | it('should return stored value', () => { 41 | const storage = new Storage() 42 | const foo = storage.set('foo', 'bar') 43 | expect(foo).to.equal('bar') 44 | }) 45 | }) 46 | 47 | describe('get()', () => { 48 | it('should return stored value', () => { 49 | const storage = new Storage() 50 | storage.set('foo', 'bar') 51 | const foo = storage.get('foo') 52 | expect(foo).to.equal('bar') 53 | }) 54 | }) 55 | 56 | describe('remove()', () => { 57 | it('should removed stored value', () => { 58 | const storage = new Storage() 59 | storage.set('foo', 'bar') 60 | storage.remove('foo') 61 | const foo = storage.get('foo') 62 | expect(foo).to.equal(undefined) 63 | }) 64 | }) 65 | 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /src/internals/utils.js: -------------------------------------------------------------------------------- 1 | import enverse from 'enverse' 2 | 3 | export const checkResponseStatus = (response) => { 4 | if (response.status >= 200 && response.status < 300) { 5 | return response 6 | } else { 7 | var error = new Error(response.statusText) 8 | error.response = response 9 | throw error 10 | } 11 | } 12 | 13 | export const parseResponseToJSON = (response) => { 14 | return response.json() 15 | } 16 | 17 | export const parseJWT = (token) => { 18 | if (!token) return null 19 | let base64Url = token 20 | let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') 21 | 22 | let parts = base64.split('.') 23 | if (parts.length != 3) return null 24 | 25 | try { 26 | let [headerRaw, payloadRaw, signatureRaw] = parts 27 | 28 | let header = JSON.parse(atob(headerRaw)) 29 | let payload = JSON.parse(atob(payloadRaw)) 30 | let signature = atob(signatureRaw) 31 | return { 32 | header, 33 | payload, 34 | signature 35 | } 36 | } catch (err) { 37 | console.error(err) 38 | return null 39 | } 40 | } 41 | 42 | export const isDefined = (value) => { 43 | return value !== undefined && value !== null 44 | } 45 | 46 | export const parseQueryString = (str) => { 47 | let obj = {}, keyValue, key, val 48 | if (typeof str !== 'string') { 49 | throw new Error('Non-string values are not allowed.') 50 | } 51 | (str || '').split('&').forEach((keyValueStr) => { 52 | if (keyValueStr) { 53 | keyValue = keyValueStr.split('=') 54 | key = decodeURIComponent(keyValue[0]) 55 | val = isDefined(keyValue[1]) ? decodeURIComponent(keyValue[1]) : true 56 | if (val === 'true' || val === true) { 57 | val = true 58 | } else if (val === 'false' || val === false) { 59 | val = false 60 | } else if (!isNaN(val)) { 61 | val = parseInt(val) 62 | } 63 | obj[key] = val 64 | } 65 | }) 66 | return obj 67 | } 68 | 69 | // HotFix: Facebook redirects back with '_=_' hash which breaks the app 70 | export const preventBadFacebookHash = () => { 71 | const fbHashAppendix = /_=_/ 72 | if (enverse.has.window) { 73 | if (fbHashAppendix.test(window.location.hash)) { 74 | window.location.hash = window.location.hash.replace(fbHashAppendix, '') 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/internals/utils.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import sinon from 'sinon' 3 | import { expect } from 'chai' 4 | import { 5 | checkResponseStatus, 6 | parseResponseToJSON, 7 | parseJWT, 8 | isDefined, 9 | parseQueryString, 10 | preventBadFacebookHash 11 | } from './utils' 12 | 13 | describe('utils', () => { 14 | 15 | describe('checkResponseStatus()', () => { 16 | it('should return response', () => { 17 | const response = { status: 200 } 18 | expect(checkResponseStatus(response)).to.equal(response) 19 | }) 20 | 21 | it('should throw error', () => { 22 | const response = { status: 400, statusText: 'broken response' } 23 | expect(() => checkResponseStatus(response)).to.throw(Error, /broken response/) 24 | }) 25 | }) 26 | 27 | 28 | describe('parseResponseToJSON()', () => { 29 | it('should return object', () => { 30 | let parsed = false 31 | const response = { json: () => { parsed = true }} 32 | parseResponseToJSON(response) 33 | expect(parsed).to.be.true 34 | }) 35 | }) 36 | 37 | describe('parseJWT()', () => { 38 | it('should parse valid JSON Web Token', () => { 39 | const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ' 40 | const token = parseJWT(jwt) 41 | expect(token).to.have.property('header').that.is.an('object') 42 | expect(token).to.have.property('payload').that.is.an('object') 43 | expect(token).to.have.property('signature').that.is.an('string') 44 | }) 45 | 46 | it('should return null for invalid token', () => { 47 | const brokenJWT = 'nR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi' 48 | expect(() => { return parseJWT(brokenJWT) }).to.not.throw(Error) 49 | expect(parseJWT(brokenJWT)).to.be.null 50 | }) 51 | 52 | }) 53 | 54 | describe('isDefined()', () => { 55 | it('should return false for undefined', () => { 56 | expect(isDefined(undefined)).to.be.false 57 | }) 58 | 59 | it('should return false for null', () => { 60 | expect(isDefined(null)).to.be.false 61 | }) 62 | 63 | it('should return true for boolean', () => { 64 | expect(isDefined(false)).to.be.true 65 | expect(isDefined(true)).to.be.true 66 | }) 67 | 68 | it('should return true for object', () => { 69 | expect(isDefined({})).to.be.true 70 | }) 71 | 72 | it('should return true for string', () => { 73 | expect(isDefined('')).to.be.true 74 | }) 75 | 76 | it('should return true for number', () => { 77 | expect(isDefined(-1)).to.be.true 78 | expect(isDefined(0)).to.be.true 79 | expect(isDefined(123.123)).to.be.true 80 | }) 81 | }) 82 | 83 | describe('parseQueryString()', () => { 84 | it('should parse query string', () => { 85 | expect(parseQueryString('foo=bar').foo).to.equal('bar') 86 | expect(parseQueryString('foo&bar')).to.deep.equal({ foo: true, bar: true }) 87 | expect(parseQueryString('foo=1&bar=2')).to.deep.equal({ foo: 1, bar: 2 }) 88 | }) 89 | 90 | it('should throws an error for invalid params', () => { 91 | const errPattern = /Non-string values are not allowed./ 92 | expect(() => parseQueryString(0)).to.throw(Error, errPattern) 93 | expect(() => parseQueryString(() => {})).to.throw(Error, errPattern) 94 | expect(() => parseQueryString({ foo: 'bar'})).to.throw(Error, errPattern) 95 | }) 96 | }) 97 | 98 | describe('should preventBadFacebookHash()', () => { 99 | it('should remove messy facebook hash', () => { 100 | window.location.hash = window.location.hash + '_=_' 101 | expect(/_=_/.test(window.location.hash)).to.be.true 102 | preventBadFacebookHash() 103 | expect(/_=_/.test(window.location.hash)).to.be.false 104 | }) 105 | }) 106 | 107 | }) 108 | -------------------------------------------------------------------------------- /src/local.js: -------------------------------------------------------------------------------- 1 | import config from './internals/config' 2 | import { fetchOpts } from './internals/defaults' 3 | import { parseResponseToJSON, checkResponseStatus } from './internals/utils' 4 | import { setToken, getToken, removeToken, getAuthHeader } from './shared' 5 | 6 | export const signup = (userData, options) => { 7 | let {baseUrl, signupUrl} = config 8 | let url = baseUrl + signupUrl 9 | let opts = Object.assign({}, fetchOpts, { 10 | method: 'POST', 11 | body: JSON.stringify(userData) 12 | }) 13 | 14 | return fetch(url, opts) 15 | .then(checkResponseStatus) 16 | .then(parseResponseToJSON) 17 | .then((data) => ({token: setToken(data.token)})) 18 | } 19 | 20 | export const login = (userData, options) => { 21 | let {baseUrl, loginUrl} = config 22 | let url = baseUrl + loginUrl 23 | let opts = Object.assign({}, fetchOpts, { 24 | method: 'POST', 25 | body: JSON.stringify(userData) 26 | }, options) 27 | 28 | return fetch(url, opts) 29 | .then(checkResponseStatus) 30 | .then(parseResponseToJSON) 31 | .then((data) => ({token: setToken(data.token)})) 32 | } 33 | 34 | export const logout = () => { 35 | return new Promise((resolve, reject) => { 36 | setTimeout(() => { 37 | if (!!getToken()) { 38 | removeToken() 39 | resolve({success: true}) 40 | } else { 41 | reject(new Error('You are trying to log out unauthenticated user.')) 42 | } 43 | }) 44 | }) 45 | } 46 | 47 | export const refreshToken = (options) => { 48 | let {baseUrl, refreshUrl} = config 49 | let url = baseUrl + refreshUrl 50 | let opts = Object.assign({}, fetchOpts, { 51 | method: 'GET', 52 | headers: Object.assign({}, 53 | fetchOpts.headers, 54 | getAuthHeader() 55 | ), 56 | }, options) 57 | return fetch(url, opts) 58 | .then(checkResponseStatus) 59 | .then(parseResponseToJSON) 60 | .then((data) => ({token: setToken(data.token)})) 61 | } 62 | 63 | export const exchangeCodeForToken = (provider, oauthData, options) => { 64 | let {baseUrl, oauthUrl} = config 65 | let url = baseUrl + oauthUrl.replace('{provider}', provider) 66 | let opts = Object.assign({}, fetchOpts, { 67 | method: 'POST', 68 | body: JSON.stringify(oauthData) 69 | }, options) 70 | return fetch(url, opts) 71 | .then(checkResponseStatus) 72 | .then(parseResponseToJSON) 73 | .then((data) => ({token: setToken(data.token)})) 74 | } -------------------------------------------------------------------------------- /src/shared.js: -------------------------------------------------------------------------------- 1 | import storage from './internals/storage' 2 | import config from './internals/config' 3 | import { parseJWT } from './internals/utils' 4 | 5 | export const setToken = (token) => { 6 | return storage.set(config.tokenName, token) 7 | } 8 | 9 | export const getToken = (asJSON = false) => { 10 | let token = storage.get(config.tokenName) 11 | if (asJSON) return parseJWT(token) 12 | return token 13 | } 14 | 15 | export const removeToken = () => { 16 | storage.remove(config.tokenName) 17 | } 18 | 19 | export const getAuthHeader = () => { 20 | let token; 21 | if (isAuthenticated() && config.authHeader && config.authToken) { 22 | let token = config.authToken + ' ' + getToken() 23 | return {[config.authHeader]: token } 24 | } 25 | return {} 26 | } 27 | 28 | export const isAuthenticated = () => { 29 | const token = parseJWT(getToken()) 30 | if (!token) return false 31 | 32 | let exp = token.payload.exp 33 | if (!exp) return true 34 | 35 | let isExpTimestamp = typeof exp === 'number' 36 | if (!isExpTimestamp) return false 37 | return Math.round(new Date().getTime() / 1000) < exp 38 | } -------------------------------------------------------------------------------- /src/user.js: -------------------------------------------------------------------------------- 1 | import config from './internals/config' 2 | import { fetchOpts } from './internals/defaults' 3 | import { parseResponseToJSON, checkResponseStatus } from './internals/utils' 4 | import { getAuthHeader } from './shared' 5 | 6 | export const getProfile = (options) => { 7 | let {baseUrl, profileUrl} = config 8 | let url = baseUrl + profileUrl 9 | let opts = Object.assign({}, fetchOpts, { 10 | method: 'GET', 11 | headers: Object.assign({}, 12 | fetchOpts.headers, 13 | getAuthHeader() 14 | ), 15 | }, options) 16 | return fetch(url, opts) 17 | .then(checkResponseStatus) 18 | .then(parseResponseToJSON) 19 | } 20 | 21 | export const updateProfile = (profileData, options) => { 22 | let {baseUrl, profileUrl} = config 23 | let url = baseUrl + profileUrl 24 | let opts = Object.assign({}, fetchOpts, { 25 | method: 'PUT', 26 | headers: Object.assign({}, 27 | fetchOpts.headers, 28 | getAuthHeader() 29 | ), 30 | body: JSON.stringify(profileData) 31 | }, options) 32 | return fetch(url, opts) 33 | .then(checkResponseStatus) 34 | } --------------------------------------------------------------------------------