├── .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 | [](https://travis-ci.org/fullstackforger/react-jwt-auth)
6 | [](https://coveralls.io/github/fullstackforger/react-jwt-auth?branch=master)
7 | [](https://codeclimate.com/github/fullstackforger/react-jwt-auth)
8 | [](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 | [](https://www.npmjs.com/package/react-jwt-auth)
10 | [](https://david-dm.org/fullstackforger/react-jwt-auth)
11 | [](https://david-dm.org/fullstackforger/react-jwt-auth?type=dev)
12 | [](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 | [](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 | ,
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 | }
--------------------------------------------------------------------------------