├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── README.md ├── demo ├── api.js └── src │ └── index.js ├── nwb.config.js ├── package-lock.json ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | demo/ 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the components's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-oauth2 2 | 3 | [![react-redux-oauth2][npm-badge]][npm] 4 | 5 | Redux OAuth Component, server rendering supported 6 | 7 | [npm-badge]: https://img.shields.io/npm/v/react-redux-oauth2.png?style=flat-square 8 | [npm]: https://www.npmjs.org/package/react-redux-oauth2 9 | 10 | 11 | ## Reducer 12 | ```js 13 | import {reducer} from 'react-redux-oauth2' 14 | combineReducers( 15 | // ... your reducers 16 | oauth: reducer 17 | ); 18 | ``` 19 | 20 | ## Usage 21 | ```js 22 | import { actions, reducer, signin, signout } from 'react-redux-oauth2' 23 | 24 | class YourComponent extends React.Component { 25 | componentWillMount () { 26 | const { dispatch } = this.props 27 | dispatch(actions.config({ 28 | client_id: 'YOUR client id', 29 | client_secret: 'YOUR client secret', 30 | url: 'http://localhost:5000/api', // your oauth server root 31 | providers: { 32 | github: '/auth/github' // provider path 33 | } 34 | })) 35 | } 36 | async handlesignin (e) { 37 | const { dispatch } = this.props 38 | e.preventdefault() 39 | await dispatch(actions.signin({ 40 | username: this.refs.username.value, 41 | password: this.refs.password.value 42 | })) 43 | } 44 | render () { 45 | const { oauth } = this.props 46 | const Signin = signin({ 47 | popup: {}, // popup settings 48 | success () {}, // invoke when signin success 49 | failed () {}, // invoke when signin failed 50 | cancel () {} // invoke when signin cancel 51 | })(props => 59 | 60 |
61 | Signin 62 |
63 | Signout 64 | 65 | ) 66 | } 67 | } 68 | ``` 69 | For a full runable example see `./demo/src` 70 | ## Run Demo & debug locally 71 | ```bash 72 | npm start 73 | ``` 74 | http://localhost:3000 for debug 75 | -------------------------------------------------------------------------------- /demo/api.js: -------------------------------------------------------------------------------- 1 | const { Koapi, middlewares, router } = require('koapi') 2 | 3 | const app = new Koapi() 4 | const route = new router.Router() 5 | route.get('/api/auth/token', async ctx => { 6 | ctx.body = { 7 | id: 1, 8 | username: 'admin', 9 | email: 'garbinh@gmail.com', 10 | avatar: 'https://avatars2.githubusercontent.com/u/63785?v=3&s=460', 11 | created_at: '2017-05-08T16:30:46.269Z', 12 | updated_at: '2017-05-08T16:30:46.269Z' 13 | } 14 | }) 15 | route.post('/api/auth/token', async ctx => { 16 | ctx.status = 201 17 | ctx.body = { 18 | 'access_token': 'fc46a2e90faf6b4b366be5e7475dee69', 19 | 'refresh_token': 'c0b65277ddbb1372d372aa7ba61b10b2', 20 | 'expires': 7200, 21 | 'token_type': 'Bearer'} 22 | }) 23 | route.del('/api/auth/token', async ctx => { 24 | ctx.status = 204 25 | }) 26 | route.get('/api/auth/github/callback', async ctx => { 27 | ctx.redirect('http://localhost:3000/?access_token=fc46a2e90faf6b4b366be5e7475dee69') 28 | }) 29 | route.get('/api/auth/connect/github', async ctx => { 30 | ctx.redirect('/api/auth/github/callback') 31 | }) 32 | app.use(middlewares.preset('restful')) 33 | app.use(route.routes()) 34 | app.use(route.allowedMethods()) 35 | app.listen(5000, e => console.log('API listening on port 5000')) 36 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render} from 'react-dom' 3 | import { connect, Provider } from 'react-redux' 4 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux' 5 | import thunk from 'redux-thunk' 6 | import { actions, reducer, signin, signout } from '../../src' 7 | 8 | const { devToolsExtension = f => f } = global.window || {} 9 | const store = createStore(combineReducers({ oauth: reducer }), compose( 10 | applyMiddleware(thunk), 11 | devToolsExtension() 12 | )) 13 | 14 | const Demo = connect(state => ({oauth: state.oauth}))(class extends React.Component { 15 | componentWillMount () { 16 | const { dispatch } = this.props 17 | dispatch(actions.config({ 18 | token: '/auth/token', 19 | client_id: '0f434d4b-06bf-4cb2-b8f4-f20bf9349beb', 20 | client_secret: '530897d5880494a6a9ac92d1273d8ba5', 21 | url: 'http://localhost:5000/api', 22 | providers: { 23 | github: '/auth/connect/github' 24 | } 25 | })) 26 | } 27 | async handleSignin (e) { 28 | const { dispatch } = this.props 29 | e.preventDefault() 30 | console.log(await dispatch(actions.signin({ 31 | username: this.refs.username.value, 32 | password: this.refs.password.value 33 | }, console.log)), '=====') 34 | } 35 | render () { 36 | const { oauth } = this.props 37 | const Signin = signin({ 38 | success (user) { 39 | console.log(user) 40 | } 41 | })(props => 58 | 59 |
60 | Signin with Github 61 |
62 | Signout 63 | 64 | 65 | ) 66 | } 67 | }) 68 | 69 | render(( 70 | 71 | 72 | 73 | ), document.querySelector('#demo')) 74 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'ReactReduxOAuth2', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-oauth2", 3 | "version": "0.5.13", 4 | "description": "react-redux-oauth2 React component", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "start": "concurrently 'nwb serve-react-demo' 'node ./demo/api'", 17 | "test": "nwb test-react", 18 | "test:coverage": "nwb test-react --coverage", 19 | "test:watch": "nwb test-react --server" 20 | }, 21 | "dependencies": { 22 | "axios": "^0.18.0", 23 | "lodash": "^4.17.4", 24 | "query-string": "^6.2.0", 25 | "react-redux": "^6.0.0", 26 | "redux": "^4.0.1", 27 | "redux-actions": "^2.0.2", 28 | "redux-thunk": "^2.2.0" 29 | }, 30 | "peerDependencies": { 31 | "react": "15.x" 32 | }, 33 | "devDependencies": { 34 | "concurrently": "^4.1.0", 35 | "koapi": "^0.10.70", 36 | "nwb": "^0.23.0", 37 | "react": "^16.7.0", 38 | "react-dom": "^16.7.0" 39 | }, 40 | "author": "", 41 | "homepage": "", 42 | "license": "MIT", 43 | "repository": "", 44 | "keywords": [ 45 | "react-component" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import axios from 'axios' 3 | import { connect } from 'react-redux' 4 | import { createAction, handleActions } from 'redux-actions' 5 | import querystring from 'query-string' 6 | import { omit, get, wrap } from 'lodash' 7 | 8 | export const actions = { 9 | config: createAction('REACT_REDUX_OAUTH2/CONFIG'), 10 | error: createAction('REACT_REDUX_OAUTH2/ERROR'), 11 | start: createAction('REACT_REDUX_OAUTH2/START'), 12 | reset: createAction('REACT_REDUX_OAUTH2/RESET'), 13 | cancel: createAction('REACT_REDUX_OAUTH2/CANCEL'), 14 | save: createAction('REACT_REDUX_OAUTH2/SAVE'), 15 | signin (creds, cb = f => f) { 16 | return (dispatch, getState) => { 17 | const { config } = getState().oauth 18 | dispatch(actions.start()) 19 | return new Promise((resolve, reject) => { 20 | axios.post(`${config.url}${config.token}`, Object.assign({ 21 | client_id: config.client_id, 22 | client_secret: config.client_secret, 23 | grant_type: 'password', 24 | scope: 'all' 25 | }, creds)).then(res => { 26 | dispatch(actions.sync(res.data, cb)) 27 | resolve(res) 28 | }).catch(e => { 29 | dispatch(actions.error(e)) 30 | reject(e) 31 | }) 32 | }) 33 | } 34 | }, 35 | signout (cb = f => f) { 36 | return (dispatch, getState) => { 37 | const { user, config } = getState().oauth 38 | return axios.delete(`${config.url}${config.token}`, { 39 | headers: { 'Authorization': `Bearer ${user.token.access_token}` } 40 | }).then(res => { 41 | dispatch(actions.reset()) 42 | cb(null, res) 43 | }).catch(e => { 44 | dispatch(actions.error(e)) 45 | cb(e) 46 | }) 47 | } 48 | }, 49 | sync (token, cb = f => f) { 50 | return (dispatch, getState) => { 51 | const { config } = getState().oauth 52 | return axios.get(`${config.url}${config.token}`, { 53 | headers: { 'Authorization': `Bearer ${token.access_token}` } 54 | }).then(res => { 55 | const user = { token, profile: res.data } 56 | dispatch(actions.save(user)) 57 | cb(null, user) 58 | }).catch(cb) 59 | } 60 | } 61 | } 62 | 63 | export const reducer = handleActions({ 64 | 'REACT_REDUX_OAUTH2/CONFIG' (state, action) { 65 | return {...state, config: action.payload} 66 | }, 67 | 'REACT_REDUX_OAUTH2/START' (state, action) { 68 | return {...state, authenticating: true} 69 | }, 70 | 'REACT_REDUX_OAUTH2/CANCEL' (state, action) { 71 | return {...state, authenticating: false} 72 | }, 73 | 'REACT_REDUX_OAUTH2/ERROR' (state, action) { 74 | return {...state, authenticating: false, error: action.payload} 75 | }, 76 | 'REACT_REDUX_OAUTH2/RESET' (state, action) { 77 | return {...state, authenticating: false, error: null, user: {token: null, profile: null}} 78 | }, 79 | 'REACT_REDUX_OAUTH2/SAVE' (state, action) { 80 | return { 81 | ...state, 82 | authenticating: false, 83 | user: action.payload 84 | } 85 | } 86 | }, { 87 | authenticating: false, 88 | user: { 89 | token: null, 90 | profile: null 91 | }, 92 | config: { 93 | url: 'http://localhost', 94 | token: '/oauth/token', 95 | client_id: null, 96 | client_secret: null, 97 | providers: { 98 | github: '/auth/github' 99 | } 100 | }, 101 | error: null 102 | }) 103 | 104 | export function signout (settings) { 105 | settings = Object.assign({ 106 | success () {}, 107 | failed () {} 108 | }, settings) 109 | return Component => { 110 | return connect(state => ({oauth: state.oauth}))(class extends React.Component { 111 | static get defaultProps () { 112 | return { 113 | onClick () {} 114 | } 115 | } 116 | handleClick () { 117 | this.props.dispatch(actions.signout((e, res) => { 118 | return e ? settings.failed(e) : settings.success(null, res) 119 | })) 120 | } 121 | render () { 122 | const { oauth, ...rest } = this.props 123 | const props = Object.assign({}, omit(rest, ['dispatch'])) 124 | props.disabled = false 125 | props.disabled = oauth.authenticating || get(oauth, 'user.profile') === null 126 | props.onClick = wrap(props.onClick, (func, e) => { 127 | this.handleClick(e) 128 | return func(e) 129 | }) 130 | return 131 | } 132 | }) 133 | } 134 | } 135 | export function signin (settings) { 136 | settings = Object.assign({ 137 | popup: { 138 | scrollbars: 'no', 139 | toolbar: 'no', 140 | location: 'no', 141 | titlebar: 'no', 142 | directories: 'no', 143 | status: 'no', 144 | menubar: 'no', 145 | top: '100', 146 | left: '100', 147 | width: '600', 148 | height: '500' 149 | }, 150 | listen: null, 151 | success () {}, 152 | cancel () {}, 153 | failed () {} 154 | }, settings) 155 | return Component => { 156 | return connect(state => ({oauth: state.oauth}))(class extends React.Component { 157 | static get defaultProps () { 158 | return { 159 | onClick () {} 160 | } 161 | } 162 | handleClick (e, provider) { 163 | const { dispatch, oauth: { config }, state } = this.props 164 | const query = state ? `?state=${state}` : '' 165 | const url = `${config.url}${config.providers[provider]}${query}` 166 | const name = 'connecting to ' + provider 167 | dispatch(actions.start()) 168 | this.listenPopup( 169 | window.open(url, name, querystring.stringify(settings.popup).replace(/&/g, ',')) 170 | ) 171 | } 172 | listenPopup (popup) { 173 | const { dispatch } = this.props 174 | if (popup.closed) { 175 | dispatch(actions.cancel()) 176 | settings.cancel() 177 | } else { 178 | const listen = () => { 179 | let token 180 | try { 181 | token = querystring.parse(popup.location.search.substr(1)) 182 | } catch (e) { } 183 | if (token && token.access_token) { 184 | dispatch(actions.sync(token, (err, user) => { 185 | if (err) { 186 | dispatch(actions.error(err)) 187 | settings.failed(err) 188 | popup.close() 189 | } else { 190 | settings.success(user) 191 | } 192 | })) 193 | popup.close() 194 | } else { 195 | setTimeout(this.listenPopup.bind(this, popup), 0) 196 | } 197 | } 198 | settings.listen ? settings.listen.call(this, popup, settings) : listen() 199 | } 200 | } 201 | render () { 202 | const {oauth, provider, ...rest} = this.props 203 | const props = Object.assign({}, omit(rest, [ 204 | 'dispatch', 'onCancel', 'onSuccess', 'onFailed' 205 | ])) 206 | props.disabled = oauth.authenticating || get(oauth, 'user.profile') !== null 207 | props.onClick = wrap(props.onClick, (func, e) => { 208 | this.handleClick(e, provider) 209 | return func(e) 210 | }) 211 | return 212 | } 213 | }) 214 | } 215 | } 216 | --------------------------------------------------------------------------------