├── .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 => )
52 | const Signout = singout()(props => )
53 | return (
54 |
55 |
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 => )
42 | const Signout = signout({
43 | success () {
44 | console.log(arguments)
45 | },
46 | failed () {
47 | console.log('error')
48 | }
49 | })(props => )
50 | return (
51 |
52 |
react-redux-oauth2 Demo
53 |
54 |
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 |
--------------------------------------------------------------------------------