Merge pull request #49 from sharetribe/sdk-login-and-logout

E2E authentication with the SDK
This commit is contained in:
Kimmo Puputti 2017-02-23 10:17:12 +02:00 committed by GitHub
commit 14d0211e86
6 changed files with 100 additions and 8 deletions

View file

@ -24,7 +24,7 @@
"sharetribe-scripts": "0.8.6",
"source-map-support": "^0.4.11",
"url": "^0.11.0",
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git"
"sharetribe-sdk": "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514"
},
"devDependencies": {
"enzyme": "^2.7.1",

View file

@ -2,10 +2,16 @@
/**
* Authentication duck.
*/
import { call, put, take, cancel, fork } from 'redux-saga/effects';
import { call, put, take, cancel, fork, takeLatest } from 'redux-saga/effects';
const authenticated = authInfo => authInfo.grantType === 'refresh_token';
// ================ Action types ================ //
export const AUTH_INFO_REQUEST = 'app/Auth/AUTH_INFO_REQUEST';
export const AUTH_INFO_SUCCESS = 'app/Auth/AUTH_INFO_SUCCESS';
export const AUTH_INFO_ERROR = 'app/Auth/AUTH_INFO_ERROR';
export const LOGIN_REQUEST = 'app/Auth/LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'app/Auth/LOGIN_SUCCESS';
export const LOGIN_ERROR = 'app/Auth/LOGIN_ERROR';
@ -16,17 +22,25 @@ export const LOGOUT_ERROR = 'app/Auth/LOGOUT_ERROR';
// ================ Reducer ================ //
const initialState = { isAuthenticated: false, error: null };
const initialState = { authInfoLoaded: false, isAuthenticated: false, error: null };
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case AUTH_INFO_REQUEST:
return { ...state, error: null };
case AUTH_INFO_SUCCESS:
return { ...state, authInfoLoaded: true, isAuthenticated: authenticated(payload) };
case AUTH_INFO_ERROR:
return { ...state, error: payload };
case LOGIN_REQUEST:
return { ...state, error: null };
case LOGIN_SUCCESS:
return { ...state, isAuthenticated: true };
case LOGIN_ERROR:
return { ...state, error: payload };
case LOGOUT_REQUEST:
return { ...state, error: null };
case LOGOUT_SUCCESS:
@ -40,6 +54,10 @@ export default function reducer(state = initialState, action = {}) {
// ================ Action creators ================ //
export const authInfo = () => ({ type: AUTH_INFO_REQUEST });
export const authInfoSuccess = info => ({ type: AUTH_INFO_SUCCESS, payload: info });
export const authInfoError = error => ({ type: AUTH_INFO_ERROR, payload: error, error: true });
export const login = (username, password) => ({
type: LOGIN_REQUEST,
payload: { username, password },
@ -53,6 +71,15 @@ export const logoutError = error => ({ type: LOGOUT_ERROR, payload: error, error
// ================ Worker sagas ================ //
export function* callAuthInfo(sdk) {
try {
const info = yield call(sdk.authInfo);
yield put(authInfoSuccess(info));
} catch (e) {
yield put(authInfoError(e));
}
}
export function* callLogin(action, sdk) {
const { payload } = action;
const { username, password } = payload;
@ -78,6 +105,10 @@ export function* callLogout(action, sdk) {
// ================ Watcher sagas ================ //
export function* watchAuthInfo(sdk) {
yield takeLatest(AUTH_INFO_REQUEST, callAuthInfo, sdk);
}
export function* watchAuth(sdk) {
let task;

View file

@ -6,6 +6,9 @@ import reducer, {
NOTHING_IN_PROGRESS,
LOGIN_IN_PROGRESS,
LOGOUT_IN_PROGRESS,
authInfo,
authInfoSuccess,
authInfoError,
login,
loginSuccess,
loginError,
@ -21,6 +24,7 @@ describe('Auth duck', () => {
describe('reducer', () => {
it('should be logged out by default', () => {
const state = reducer();
expect(state.authInfoLoaded).toEqual(false);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
});
@ -50,6 +54,36 @@ describe('Auth duck', () => {
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toEqual(error);
});
it('should set initial state for unauthenticated users', () => {
const authInfoLoggedOut = {};
const initialState = reducer();
expect(initialState.authInfoLoaded).toEqual(false);
const state = reducer(initialState, authInfoSuccess(authInfoLoggedOut));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
});
it('should set initial state for anonymous users', () => {
const authInfoAnonymous = { grantType: 'client_credentials' };
const initialState = reducer();
expect(initialState.authInfoLoaded).toEqual(false);
const state = reducer(initialState, authInfoSuccess(authInfoAnonymous));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(false);
expect(state.error).toBeNull();
});
it('should set initial state for unauthenticated users', () => {
const authInfoLoggedIn = { grantType: 'refresh_token' };
const initialState = reducer();
expect(initialState.authInfoLoaded).toEqual(false);
const state = reducer(initialState, authInfoSuccess(authInfoLoggedIn));
expect(state.authInfoLoaded).toEqual(true);
expect(state.isAuthenticated).toEqual(true);
expect(state.error).toBeNull();
});
});
describe('login worker', () => {

View file

@ -21,6 +21,7 @@ import { matchPathname } from './util/routes';
import * as sample from './util/sample';
import createRootSaga from './sagas';
import config from './config';
import { authInfo } from './ducks/Auth.duck';
import {
showListings,
queryListings,
@ -31,6 +32,23 @@ import {
import './index.css';
const renderWithAuthInfo = store => {
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
};
// Wait for the store to have the Auth.authInfoLoaded flag, render the
// application when the auth information is present.
const renderWhenAuthInfoLoaded = store => {
const unsubscribe = store.subscribe(() => {
const authInfoLoaded = store.getState().Auth.authInfoLoaded;
if (authInfoLoaded) {
unsubscribe();
renderWithAuthInfo(store);
}
});
store.dispatch(authInfo());
};
// If we're in a browser already, render the client application.
if (typeof window !== 'undefined') {
// eslint-disable-next-line no-underscore-dangle
@ -40,7 +58,16 @@ if (typeof window !== 'undefined') {
store.runSaga(createRootSaga(sdk));
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
const authInfoLoaded = store.getState().Auth.authInfoLoaded;
// If the server already loaded the auth information, render the app
// immediately. Otherwise wait for the flag to be loaded and render
// when auth information is present.
if (authInfoLoaded) {
renderWithAuthInfo(store);
} else {
renderWhenAuthInfoLoaded(store);
}
// Expose stuff for the browser REPL
if (config.dev) {

View file

@ -1,9 +1,9 @@
import { watchAuth } from './ducks/Auth.duck';
import { watchAuthInfo, watchAuth } from './ducks/Auth.duck';
import { watchLoadListings } from './containers/SearchPage/SearchPage.duck';
import { watchSdk } from './ducks/sdk.duck';
const createRootSaga = sdk => function* rootSaga() {
yield [watchAuth(sdk), watchLoadListings(sdk), watchSdk(sdk)];
yield [watchAuthInfo(sdk), watchAuth(sdk), watchLoadListings(sdk), watchSdk(sdk)];
};
export default createRootSaga;

View file

@ -5675,9 +5675,9 @@ sharetribe-scripts@0.8.6:
optionalDependencies:
fsevents "1.0.14"
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git":
"sharetribe-sdk@git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514":
version "0.0.1"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js.git#037a4328da4926428f6d36f19d349186d06f8aad"
resolved "git+ssh://git@github.com/sharetribe/sharetribe-sdk-js#eab535d1e489e262c72c9fb822a8aac36f22e514"
dependencies:
axios "^0.15.3"
js-cookie "^2.1.3"