Skip to content

Commit

Permalink
auth example poc
Browse files Browse the repository at this point in the history
  • Loading branch information
tomkis committed Feb 16, 2016
1 parent 13c9dce commit d81b487
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 0 deletions.
3 changes: 3 additions & 0 deletions examples/auth/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015", "stage-2", "react"]
}
11 changes: 11 additions & 0 deletions examples/auth/dev/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>redux-saga-rxjs</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="/app.bundle.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions examples/auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "redux-saga-rxjs-example-auth",
"version": "0.1.0",
"scripts": {
"start": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --port 3000 --hot --content-base ./dev"
},
"devDependencies": {
"babel-cli": "^6.5.1",
"babel-core": "^6.5.2",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.2",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-2": "^6.5.0",
"webpack": "^1.12.4",
"webpack-dev-server": "^1.12.1"
},
"dependencies": {
"babel-runtime": "^6.5.0",
"moment": "^2.11.2",
"react": "^0.14.2",
"react-dom": "^0.14.2",
"react-redux": "^4.0.0",
"redux": "^3.0.4",
"redux-saga-rxjs": "^0.2.0",
"rxjs": "^5.0.0-beta.2"
},
"author": "Tomas Weiss <tomas.weiss2@gmail.com>",
"license": "MIT"
}
9 changes: 9 additions & 0 deletions examples/auth/src/actions/actionCreators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Actions from '../constants/actions';

export const logIn = credentials => ({type: Actions.LOG_IN, payload: credentials});
export const logOut = () => ({type: Actions.LOG_OUT, payload: null});
export const changeCredentials = credentials => ({type: Actions.CHANGE_CREDENTIALS, payload: credentials});
export const tokenRefreshed = refreshed => ({type: Actions.TOKEN_REFRESHED, payload: refreshed});
export const hideToast = () => ({type: Actions.HIDE_TOAST, payload: null});
export const loggedIn = (credentials, refreshed) => ({type: Actions.LOGGED_IN, payload: { credentials, refreshed }});
export const logInFailure = () => ({type: Actions.LOG_IN_FAILURE, payload: null});
31 changes: 31 additions & 0 deletions examples/auth/src/components/Application.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { connect } from 'react-redux';

import * as ActionCreators from '../actions/actionCreators';

export default connect(appState => appState)(({ dispatch, credentials, loginError, loggedIn, lastTokenRefresh, apiInProgress }) => {
if (apiInProgress) {
return <div>API call in progress</div>;
} else if (loggedIn) {
return (
<div>
<div>Token last refreshed {lastTokenRefresh}</div>
<button onClick={() => dispatch(ActionCreators.logOut())}>Log out</button>
</div>
);
} else {
return (
<div>
<label htmlFor="credentials">Credentials (valid credentials is <b>saga</b>):&nbsp;</label>
<input
id="credentials"
type="text"
value={credentials}
onChange={ev => dispatch(ActionCreators.changeCredentials(ev.target.value))}
/><br />
<button onClick={() => dispatch(ActionCreators.logIn(credentials))}>Log in</button><br />
{loginError ? <span style={{color: 'red'}}>Invalid credentials provided</span> : false}
</div>
);
}
});
12 changes: 12 additions & 0 deletions examples/auth/src/constants/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// User actions
export const LOG_IN = 'LOG_IN';
export const LOG_OUT = 'LOG_OUT';
export const CHANGE_CREDENTIALS = 'CHANGE_CREDENTIALS';

// Saga actions
export const TOKEN_REFRESHED = 'TOKEN_REFRESHED';
export const HIDE_TOAST = 'HIDE_TOAST';

// API callbacks
export const LOGGED_IN = 'LOGGED_IN';
export const LOG_IN_FAILURE = 'LOG_IN_FAILURE';
17 changes: 17 additions & 0 deletions examples/auth/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import sagaMiddleware from 'redux-saga-rxjs';

import Application from './components/Application';
import rootReducer from './reducers/rootReducer';
import authSaga from './sagas/authSaga';

const store = createStore(rootReducer, undefined, applyMiddleware(sagaMiddleware(authSaga)));

render((
<Provider store={store}>
<Application />
</Provider>
), document.getElementById('app'));
38 changes: 38 additions & 0 deletions examples/auth/src/reducers/rootReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as Actions from '../constants/actions';

const initialAppState = {
loggedIn: false,
lastTokenRefresh: '',
apiInProgress: false,
credentials: 'saga',
loginError: false
};

export default (appState = initialAppState, { type, payload }) => {

switch (type) {
case Actions.CHANGE_CREDENTIALS:
return { ...appState, credentials: payload };

case Actions.LOG_IN:
return { ...appState, apiInProgress: true };

case Actions.LOG_OUT:
return initialAppState;

case Actions.LOGGED_IN:
return { ...appState, loggedIn: true, apiInProgress: false, lastTokenRefresh: payload.refreshed };

case Actions.LOG_IN_FAILURE:
return { ...appState, loggedIn: false, apiInProgress: false, loginError: true };

case Actions.TOKEN_REFRESHED:
return { ...appState, lastTokenRefresh: payload };

case Actions.HIDE_TOAST:
return { ...appState, loginError: false };

default:
return appState;
}
};
73 changes: 73 additions & 0 deletions examples/auth/src/sagas/authSaga.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Observable } from 'rxjs';
import moment from 'moment';

import * as Actions from '../constants/actions';
import * as ActionCreators from '../actions/actionCreators';

const logInApi = crendetials => new Promise((res, rej) => setTimeout(() => {
if (crendetials === 'saga') {
res(moment().format('HH:mm:ss'));
} else {
rej('Invalid credentials');
}
}, 500));

const createDelay = time => new Promise(res => setTimeout(() => res(), time));
const actionOrder = (actions, order) => actions.every(({ action }, index) => action.type === order[index]);
const actionPredicate = actions => ({ action }) => actions.some(someAction => someAction === action.type);

const AUTH_EXPIRATION = 1000;

const LOG_OUT_ACTIONS_ORDER = [
Actions.LOG_IN,
Actions.LOGGED_IN,
Actions.LOG_OUT
];

// User clicked the log in button,
// call the API and respond either success or failure
const authGetTokenSaga = iterable => iterable
.filter(actionPredicate([Actions.LOG_IN]))
.flatMap(({ action }) => Observable
.fromPromise(logInApi(action.payload))
.map(refreshed => ActionCreators.loggedIn(action.payload, refreshed))
.catch(() => Observable.of(ActionCreators.logInFailure())));

// After the user is successfuly logged in,
// let's schedule an infinite interval stream
// which can be interrupted either by LOG_OUT
// or failure in refreshing (TOKEN_REFRESHING_FAILED)
const authRefreshTokenSaga = iterable => iterable
.filter(actionPredicate([Actions.LOGGED_IN]))
.flatMap(({ action }) => Observable
.interval(AUTH_EXPIRATION)
.flatMap(() => Observable
.fromPromise(logInApi(action.payload.credentials))
.map(refreshed => ActionCreators.tokenRefreshed(refreshed))
)
.takeUntil(iterable.filter(actionPredicate([Actions.LOG_OUT])))
);

// Observe all the actions in specific order
// to determine whether user wants to log out
const authHandleLogOutSaga = iterable => iterable
.filter(actionPredicate(LOG_OUT_ACTIONS_ORDER))
.bufferCount(LOG_OUT_ACTIONS_ORDER.length)
.filter(actions => actionOrder(actions, LOG_OUT_ACTIONS_ORDER))
.map(() => ActionCreators.logOut());

const authShowLogInFailureToast = iterable => iterable
.filter(actionPredicate([Actions.LOG_IN_FAILURE]))
.flatMap(() =>
Observable.race(
Observable.fromPromise(createDelay(5000)),
iterable.filter(actionPredicate([Actions.CHANGE_CREDENTIALS]))
)
.map(() => ActionCreators.hideToast()));

export default iterable => Observable.merge(
authGetTokenSaga(iterable),
authRefreshTokenSaga(iterable),
authHandleLogOutSaga(iterable),
authShowLogInFailureToast(iterable)
);
30 changes: 30 additions & 0 deletions examples/auth/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
var path = require('path');
var webpack = require('webpack');

module.exports = {
debug: true,
target: 'web',
devtool: 'sourcemap',
plugins: [
new webpack.NoErrorsPlugin()
],
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./src/main.js'
],
output: {
path: path.join(__dirname, './dev'),
filename: 'app.bundle.js'
},
module: {
loaders: [{
test: /\.jsx$|\.js$/,
loaders: ['babel-loader'],
include: path.join(__dirname, './src')
}]
},
resolve: {
extensions: ['', '.js', '.jsx']
}
};

0 comments on commit d81b487

Please sign in to comment.